liunx内核同步场景以及解决办法 作者: jlb 2022年10月19日 [TOC] 内核里边情况稍微复杂一点,处理进程的执行环境还有中断的执行环境,并且中断上下文的执行权限比较高,可以随时打断进程的执行,所以同步机制就显得比较复杂,在内核中提供了不同的同步机制,不如说信号量、自旋锁、读-复制-更新(RCU)、原子变量等等。这些同步机制有所适应的不同场景。在使用的时候根据不同的情况选择不同的锁机制。 ## 进程和进程之间的锁机制——信号量 `并发执行单元`对`共享资源`的同时访问会引发竟态问题。竟态问题的解决途径是保证对比共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源时,其他执行单元被禁止访问。 访问共享资源的代码区叫做“临界区”临界区需要用某种`内核同步方法`来保护。 ### semaphore(信号量)的使用 * 更改`helloDev`驱动: 在驱动中添加信号量 ``` #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> #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 "dvice is busy, hello_open fail"); return -EBUSY; } open_count++; // 临界区代码结束 up(&sema); // 取消锁/释放锁 printk(KERN_INFO "hello_open\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--; return 0; } ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l) { printk(KERN_INFO "hello_writr\r\n"); int writelen = 0; writelen = BUFFER_MAX>s ? s: BUFFER_MAX; if (copy_from_user(buffer, u, writelen)){ // copy_from_user 返回拷贝失败的字节数 return -EFAULT; } return writelen; // 返回拷贝成功的字节数 } ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l) { printk(KERN_INFO "hello_writr\r\n"); int readlen; readlen = BUFFER_MAX>s?s:BUFFER_MAX; if(copy_to_user(u, buffer, readlen)) // copy_to_user 返回拷贝失败的字节数 { return -EFAULT; } return readlen; // 返回拷贝成功的字节数 } int hello_init(void) { devNum = MKDEV(reg_major, reg_minor); // 手动生成设备号 if(OK == register_chrdev_region(devNum, subDevNum, "hellworrd")) // register_chrdev_region 设备号注册到内核中,注册后别的设备就不能使用了 { printk(KERN_INFO "register_chrdev_region ok \n"); }else{ printk(KERN_INFO "register_chrdev_region error \n"); return ERROR; } printk(KERN_INFO " hello driver init \n"); gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL); // 申请结构体cdve代表字符设备 gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL); // 申请结构体file_operations 表示文件的一些操作 // 绑定file_operations的操作 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, 1); // 建立字符设备和设备号联系 // 设备号,文件操作,字符设备建立关联 sema_init(&sema, 1); // 初始化时初始化信号量,初始化为1,是因为共享资源只有一个。 return 0; } void __exit hello_exit(void) { // 卸载,跟注册是一一对应的 printk(KERN_INFO " hello driver exit \n"); cdev_del(gDev); kfree(gFile); kfree(gDev); unregister_chrdev_region(devNum, subDevNum); return; } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); ``` 然后对该驱动进行编译(make)和安装(insmode) ### 测试 * 驱动测试文件,还是原有的`write.c`在打开时添加休眠时间。 ```C/C++ #include <fcntl.h> #include <stdio.h> #include <string.h> #include <sys/select.h> #define DATA_MUN (32) int main(int argc, char const *argv[]) { int fd, i; int r_len, w_len; fd_set fdset; char buf[DATA_MUN] = "hello world"; fd = open("/dev/hello", O_RDWR); if (-1 == fd){ perror("open file error\r\n"); return -1; }else{ printf("open success\n"); } w_len = write(fd, buf, DATA_MUN); if (w_len == -1){ perror("write error\r\n"); return-1; } sleep(5); // 休眠5秒 printf("write len: %d\r\n", w_len); close(fd); return 0; } ``` * 运行测试程序 进程1打开,成功,在`sleep`期间运行时再次打开提示失败提示`: Device or resource busy` 第一个进程运行时:  运行第二个进程,结果打开失败:  **通过这种方式可以实现对设备独占式的访问,但是这个只是其中的一种场景。信号量在内核中适用与进程之间的通讯。** **信号量用于进程之间同步,进程在信号量保护临界区代码里是可以休眠的,这是和自旋锁的最大区别。** ### 信号量的特点: 1. 用于进程和进程之间的同步 2. 允许有多个进程进去临界区代码执行 3. 进程获取不到信号量锁会进入休眠,并让出cpu 4. **被信号量锁保护的临界区代码允许休眠** 5. 本质是基于进程调度器,UP(单核CPU)和SMP(双核或多核CPU)下的实现无差异 6. **不支持进程和中断之间的同步** ## 内核原子变量 **原子变量(atomic)适用与针对int变量进行同步的场景** * 更改`helloDev`驱动: 在驱动中添原子变量实现同步 ``` #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> #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]; static atomic_t can_open = ATOMIC_INIT(1); // 使用原子变量把 int hello_open(struct inode *p, struct file *f) { if(!atomic_dec_and_test(&can_open)){ // atomic_dec_and_test 是一个原子操作 会先给原子变量-1,如果结果是0的话返回真否则返回假 printk(KERN_INFO "dvice is busy, hello_open fail"); atomic_inc(&can_open); // atomic_inc 是一个原子操作 会先给原子变量+1 return -EBUSY; } printk(KERN_INFO "hello_open\r\n"); return 0; } int hello_close(struct inode *inode, struct file *filp){ atomic_inc(&can_open); // atomic_inc 是一个原子操作 会先给原子变量+1 printk(KERN_INFO "something wrong, hello_close fail"); return 0; } ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l) { printk(KERN_INFO "hello_writr\r\n"); int writelen = 0; writelen = BUFFER_MAX>s ? s: BUFFER_MAX; if (copy_from_user(buffer, u, writelen)){ // copy_from_user 返回拷贝失败的字节数 return -EFAULT; } return writelen; // 返回拷贝成功的字节数 } ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l) { printk(KERN_INFO "hello_writr\r\n"); int readlen; readlen = BUFFER_MAX>s?s:BUFFER_MAX; if(copy_to_user(u, buffer, readlen)) // copy_to_user 返回拷贝失败的字节数 { return -EFAULT; } return readlen; // 返回拷贝成功的字节数 } int hello_init(void) { devNum = MKDEV(reg_major, reg_minor); // 手动生成设备号 if(OK == register_chrdev_region(devNum, subDevNum, "hellworrd")) // register_chrdev_region 设备号注册到内核中,注册后别的设备就不能使用了 { printk(KERN_INFO "register_chrdev_region ok \n"); }else{ printk(KERN_INFO "register_chrdev_region error \n"); return ERROR; } printk(KERN_INFO " hello driver init \n"); gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL); // 申请结构体cdve代表字符设备 gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL); // 申请结构体file_operations 表示文件的一些操作 // 绑定file_operations的操作 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, 1); // 建立字符设备和设备号联系 // 设备号,文件操作,字符设备建立关联 sema_init(&sema, 1); // 初始化时初始化信号量,初始化为1,是因为共享资源只有一个。 return 0; } void __exit hello_exit(void) { // 卸载,跟注册是一一对应的 printk(KERN_INFO " hello driver exit \n"); cdev_del(gDev); kfree(gFile); kfree(gDev); unregister_chrdev_region(devNum, subDevNum); return; } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); ``` ## spinlock(自旋锁) ### 特点 1. spinlock是一种`死等`的锁机制 2. semaphore可以允许多个执行单元进入,spinlock不行,一次只能有一个执行单元获取锁并进去临界区,其他的执行单元都是在门口不断的`死等`. 3. 执行时间短,由于spinlock`死等`这种特性,如果临界区执行时间太长,那么不断在临界区门口`死等`的那些执行单元是多么的浪费CPU啊 4. 可以在中断上下文执行,由于不睡眠,因此spinlock可以在中断上下文中适用。 PS:中断上下文不允许睡眠,也不允许调用那些可能会引起睡眠的函数。 ### 实现自选锁, 修改`hellDev.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> #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; // 定义信号量 spinlock_t count_lock; int open_count = 0; // 临界区共享变量 int hello_open(struct inode *p, struct file *f) { /* 第一个进程进来会获取锁,进到临界区代码去执行。 如果第一个进程没有释放锁时第二个进程进来时发现已加锁就只能一直等待,直到第一个进程把锁释放才能访问。 */ // down(&sema); // 加锁/获取一个锁 spin_lock(&count_lock); // 获取自旋锁 // 临界区代码开始 if(open_count >= 1){ // 临界区代码结束 // up(&sema); // 取消锁/释放锁 spin_unlock(&count_lock); // 释放锁 printk(KERN_INFO "dvice is busy, hello_open fail"); return -EBUSY; } open_count++; // 临界区代码结束 // up(&sema); // 取消锁/释放锁 spin_unlock(&count_lock); // 释放锁 printk(KERN_INFO "hello_open\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--; return 0; } ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l) { printk(KERN_INFO "hello_writr\r\n"); int writelen = 0; writelen = BUFFER_MAX>s ? s: BUFFER_MAX; if (copy_from_user(buffer, u, writelen)){ // copy_from_user 返回拷贝失败的字节数 return -EFAULT; } return writelen; // 返回拷贝成功的字节数 } ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l) { printk(KERN_INFO "hello_writr\r\n"); int readlen; readlen = BUFFER_MAX>s?s:BUFFER_MAX; if(copy_to_user(u, buffer, readlen)) // copy_to_user 返回拷贝失败的字节数 { return -EFAULT; } return readlen; // 返回拷贝成功的字节数 } int hello_init(void) { devNum = MKDEV(reg_major, reg_minor); // 手动生成设备号 if(OK == register_chrdev_region(devNum, subDevNum, "hellworrd")) // register_chrdev_region 设备号注册到内核中,注册后别的设备就不能使用了 { printk(KERN_INFO "register_chrdev_region ok \n"); }else{ printk(KERN_INFO "register_chrdev_region error \n"); return ERROR; } printk(KERN_INFO " hello driver init \n"); gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL); // 申请结构体cdve代表字符设备 gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL); // 申请结构体file_operations 表示文件的一些操作 // 绑定file_operations的操作 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, 1); // 建立字符设备和设备号联系 // 设备号,文件操作,字符设备建立关联 // sema_init(&sema, 1); // 初始化时初始化信号量,初始化为1,是因为共享资源只有一个。 spin_lock_init(&count_lock); // 初始化自旋锁 return 0; } void __exit hello_exit(void) { // 卸载,跟注册是一一对应的 printk(KERN_INFO " hello driver exit \n"); cdev_del(gDev); kfree(gFile); kfree(gDev); unregister_chrdev_region(devNum, subDevNum); return; } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); ``` 测试结果与信号量结果相同 ### 总结: ``` void spin_lock(spinlock_t* lock) // 进程和进程之间的同步 void spin_lock_bh(spinlock_t* lock) // 涉及到软件和本地软中断之间的同步 void spin_lock_irq(spinlock_t* lock) // 涉及和本地硬件中断之间的同步 void spin_lock_irqsave(lovk, flags) // 涉及和本地硬件之间的同步并保存本地中断状态 int spin_try_lock(spinlock_t* lock) // 尝试获取锁,如果成功返回非零值,否则返回零值 ``` 课程地址:[简说linux: linux内核开发50讲](https://space.bilibili.com/646178510/channel/collectiondetail?sid=375089)