- 1 信号量
- 2 驱动程序和测试程序
- 3 内核的具体实现
- 总结
Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已经被占用的信号量时,信号量会将其放到一个等待队列,然后让其睡眠,这时处理器去执行其他代码。当持有信号量的进程将信号量释放后,处于等待队列中的那个任务将被唤醒,并获得该信号量。
信号量定义在文件include/linux/semaphore.h中
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
信号量可以同时允许任意数量的锁持有者,信号量同时允许的持有者数量可以在声明信号量时指定,这个值称为使用者数量。通常情况下,信号量和自旋锁一样,在一个时刻仅允许有一个锁持有者。当数量等于1,这样的信号量被称为二值信号量或者被称为互斥信号量;初始化时也可以把数量设置为大于1的非0值,这种情况,信号量被称为计数信号量,它允许在一个时刻至多有count个锁持有者。
信号量支持两个原子操作P()和V()。前者叫做测试操作,后者叫做增加操作,后来系统把这两种操作分别叫做down()和up(),Linux也遵从这种叫法。down()通过对信号量减1来请求一个信号量,如果减1结果是0或者大于0,那么就获得信号量锁,任务就可以进入临界区,如果结果是负的,那么任务会被放入等待队列。相反,当临界区的操作完成后,up()操作用来释放信号量,如果在该信号量上的等待队列不为空,那么处于队列中等待的任务被唤醒。
信号量的操作函数如下:
在驱动中,我们仅允许一个进程打开设备,这个功能用互斥信号量来实现。
先执行:
sudo mknod /dev/hello c 232 0
驱动程序semaphore.c:
#include#include #include #include #include #include #include #include #include #define BUFFER_MAX (64) #define OK (0) #define ERROR (-1) struct cdev *gDev; struct file_operations *gFile; dev_t devNum; unsigned int subDevNum = 1; int reg_major = 232; int reg_minor = 0; char buffer[BUFFER_MAX]; struct semaphore sema; int open_count = 0; int hello_open(struct inode *p, struct file *f) { down(&sema); if(open_count>=1) { up(&sema); printk(KERN_INFO "device is busy,hello_open fail"); return -EBUSY; } open_count++; up(&sema); printk(KERN_INFO"hello_open okrn"); return 0; } int hello_close(struct inode *inode,struct file *filp) { if(open_count!=1) { printk(KERN_INFO"something wrong,hello_close fail"); return -EFAULT; } open_count--; printk(KERN_INFO"hello_close okrn"); return 0; } ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l) { int writelen =0; printk(KERN_EMERG"hello_writern"); writelen = BUFFER_MAX>s?s:BUFFER_MAX; if(copy_from_user(buffer,u,writelen)) { return -EFAULT; } return writelen; } ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l) { int readlen; printk(KERN_EMERG"hello_readrn"); readlen = BUFFER_MAX>s?s:BUFFER_MAX; if(copy_to_user(u,buffer,readlen)) { return -EFAULT; } return readlen; } int hello_init(void) { devNum = MKDEV(reg_major, reg_minor); if(OK == register_chrdev_region(devNum, subDevNum, "helloworld")){ printk(KERN_EMERG"register_chrdev_region ok n"); }else { printk(KERN_EMERG"register_chrdev_region error n"); return ERROR; } printk(KERN_EMERG" hello driver init n"); gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL); gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL); gFile->open = hello_open; gFile->release = hello_close; gFile->read = hello_read; gFile->write = hello_write; gFile->owner = THIS_MODULE; cdev_init(gDev, gFile); cdev_add(gDev, devNum, 3); sema_init(&sema,1); return 0; } void __exit hello_exit(void) { printk(KERN_INFO"hello driver exitn"); cdev_del(gDev); kfree(gDev); unregister_chrdev_region(devNum, subDevNum); return; } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL");
测试程序test.c:
#include#include #include #include #include #include #define DATA_NUM (64) int main(int argc, char *argv[]) { int fd, i; int r_len, w_len; fd_set fdset; char buf[DATA_NUM]="hello world"; fd = open("/dev/hello", O_RDWR); if(-1 == fd) { perror("open file errorrn"); return -1; } else { printf("open successern"); } w_len = write(fd,buf, DATA_NUM); if(w_len==-1) { perror("write errorn"); return -1; } sleep(5); printf("write len:%dn",w_len); close(fd); return 0; }
Makefile:
obj-m := semaphore.o KERNELDIR := /lib/modules/$(shell uname -r)/build all default:modules install:modules_install modules modules_install help clean: $(MAKE) -C $(KERNELDIR) M=$(shell pwd) $@ test:test.c gcc $^ -o $@
执行命令:
make make test
当我们同时打开两个测试时,只有一个能打开,另一个打开失败,实现了互斥访问。
信号量定义在文件include/linux/semaphore.h中,下面的函数也定义在这个文件中
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
初始化函数
static inline void sema_init(struct semaphore *sem, int val)
{
static struct lock_class_key __key;
*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}
该初始化会将val值赋值给struct semaphore里的count,wait_list初始化为链表头,lock值设定为解锁状态,lock是自旋锁。
down函数的实现在kernel/locking/semaphore.c文件中
void down(struct semaphore *sem)
{
unsigned long flags;
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;
else
__down(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
首先是raw_spin_lock_irqsave加锁,接着判断count是不是大于0,大于0就count就减去1,否则,转到__down函数执行
static noinline void __sched __down(struct semaphore *sem)
{
__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
TASK_UNINTERRUPTIBLE表示进程不可中断,MAX_SCHEDULE_TIMEOUT表示休眠时间
static inline int __sched __down_common(struct semaphore *sem, long state,
long timeout)
{
struct task_struct *task = current;
struct semaphore_waiter waiter;
list_add_tail(&waiter.list, &sem->wait_list);
waiter.task = task;
waiter.up = false;
for (;;) {
if (signal_pending_state(state, task))
goto interrupted;
if (unlikely(timeout <= 0))
goto timed_out;
__set_task_state(task, state);
raw_spin_unlock_irq(&sem->lock);
timeout = schedule_timeout(timeout);
raw_spin_lock_irq(&sem->lock);
if (waiter.up)
return 0;
}
timed_out:
list_del(&waiter.list);
return -ETIME;
interrupted:
list_del(&waiter.list);
return -EINTR;
}
semaphore_waiter 的实例表示信号的一个等待者
struct semaphore_waiter {
struct list_head list;
struct task_struct *task;
bool up;
};
list_head是一个双向链表。
__down会先将进程加入到信号的等待队列中,然后将进程设置为不可打断的睡眠状态,接着让出CPU,在指定的时间用完以后或者其它事件到达并唤醒进程,如果等待进程waiter的up不为真,将一直for循环,直到up为真,返回0。
所以down函数的功能就是先判断count是否大于0(即是否还有资源),如果大于0,减1,继续执行,否则就调用__down,将进程加入信号的等待队列中,一直for循环,直到up为真,然后继续执行。
up函数的实现也在kernel/locking/semaphore.c文件中
void up(struct semaphore *sem)
{
unsigned long flags;
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(list_empty(&sem->wait_list)))
sem->count++;
else
__up(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
首先判断信号的等待队列是否为空,为空直接让count加1,否则进入__up函数:
static noinline void __sched __up(struct semaphore *sem)
{
struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list,
struct semaphore_waiter, list);
list_del(&waiter->list);
waiter->up = true;
wake_up_process(waiter->task);
}
__up函数首先拿到等待该信号的第一个进程,在等待队列中删除该进程,并且将up置为true,最后唤醒该进程。
总结信号量会让进程休眠,让出CPU,这个时候有进程调度,进程调度开销比较大,并且不能在中断处理程序中使用信号量,因为信号量会睡眠。



