信号量
- 1 信号量
- 2 驱动程序和测试程序
- 3 内核的具体实现
- 总结
1 信号量
Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已经被占用的信号量时,信号量会将其放到一个等待队列,然后让其睡眠,这时处理器去执行其他代码。当持有信号量的进程将信号量释放后,处于等待队列中的那个任务将被唤醒,并获得该信号量。
信号量定义在文件include/linux/semaphore.h中
/* Please don't access any members of this structure directly */
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()操作用来释放信号量,如果在该信号量上的等待队列不为空,那么处于队列中等待的任务被唤醒。
信号量的操作函数如下:
2 驱动程序和测试程序
在驱动中,我们仅允许一个进程打开设备,这个功能用互斥信号量来实现。
先执行:
sudo mknod /dev/hello c 232 0
驱动程序semaphore.c:
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/semaphore.h>#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 ok\r\n");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 ok\r\n");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_write\r\n");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_read\r\n"); 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 exit\n");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 <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/select.h>
#include <unistd.h>#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 error\r\n");return -1;} else {printf("open successe\r\n");}w_len = write(fd,buf, DATA_NUM);if(w_len==-1){perror("write error\n");return -1;}sleep(5);printf("write len:%d\n",w_len);close(fd);return 0;
}
Makefile:
obj-m := semaphore.oKERNELDIR := /lib/modules/$(shell uname -r)/buildall default:modules
install:modules_installmodules modules_install help clean:$(MAKE) -C $(KERNELDIR) M=$(shell pwd) $@test:test.cgcc $^ -o $@
执行命令:
make
make test
当我们同时打开两个测试时,只有一个能打开,另一个打开失败,实现了互斥访问。
3 内核的具体实现
信号量定义在文件include/linux/semaphore.h中,下面的函数也定义在这个文件中
/* Please don't access any members of this structure directly */
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文件中
/*** down - acquire the semaphore* @sem: the semaphore to be acquired** Acquires the semaphore. If no more tasks are allowed to acquire the* semaphore, calling this function will put the task to sleep until the* semaphore is released.** Use of this function is deprecated, please use down_interruptible() or* down_killable() instead.*/
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;/* 将等待信号量的等待者加入到信号量的等待队列wait_list中 */list_add_tail(&waiter.list, &sem->wait_list);waiter.task = task;waiter.up = false;for (;;) {/* 检查有无信号打断 */if (signal_pending_state(state, task))goto interrupted;/* 检查timeout是否小于0 */if (unlikely(timeout <= 0))goto timed_out;/* 设置进程的状态 */__set_task_state(task, state);/* 解锁 */raw_spin_unlock_irq(&sem->lock);/* schedule_timeout用来让出CPU;在指定的时间用完以后或者其它事件到达并唤醒进程(比如接收了一个信号量)时,该进程才可以继续运行 */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文件中
/*** up - release the semaphore* @sem: the semaphore to release** Release the semaphore. Unlike mutexes, up() may be called from any* context and even by tasks which have never called down().*/
void up(struct semaphore *sem)
{unsigned long flags;raw_spin_lock_irqsave(&sem->lock, flags);/* 判断信号的等待队列是否为空,为空直接让count加1 */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,这个时候有进程调度,进程调度开销比较大,并且不能在中断处理程序中使用信号量,因为信号量会睡眠。