Chinaunix首页 | 论坛 | 博客
  • 博客访问: 806380
  • 博文数量: 281
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 2770
  • 用 户 组: 普通用户
  • 注册时间: 2009-08-02 19:45
个人简介

邮箱:zhuimengcanyang@163.com 痴爱嵌入式技术的蜗牛

文章分类
文章存档

2020年(1)

2018年(1)

2017年(56)

2016年(72)

2015年(151)

分类: 嵌入式

2015-07-01 10:44:22

进入第五章的学习了。


对并发的管理是操作系统编程中核心的问题之一。
并发相关的缺陷是最容易制造的,也是最难找的。
在早期的linux内核中,不支持SMP系统,导致并发执行的唯一原因是对硬件中断的服务。
但为了使用更多处理器,处理更多的任务,这种并发处理的情况更加复杂,这使得驱动开发者必须在开始设计的时候就要考虑到并发因素,并且还必须对内核提供的并发管理有坚实的理解。

scull的缺陷

scull驱动程序的write方法中,有如下代码: 

if (!dptr->data) {
    dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
    if (!dptr->data)
        goto out;
    memset(dptr->data, 0, qset * sizeof(char *));
}

假设有两个进程A和B,同时进行对内存分配操作,执行语句:dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL),对dptr->data进行赋值,则结果是第二个完成赋值的进程会“胜出”,如果进程A先赋值,则它的赋值会被进程B覆盖。这样,scull会完全忘记由A分配的内存,而只会记录进程B分配得到的指针。因此,由A分配的内存将丢失,从而永远不会返回到系统中。

上述事件过程就是一种“竟态”(race condition)。
竟态会导致对共享数据的非控制访问。竟态的后果:可能导致内存泄露,系统崩溃,数据被破坏或者产生安全问题,其结果可能是灾难性的。

并发及管理

在现代linux系统中存在大量并发的来源,因此都会导致可能的竟态,比如:

(1)多个用户空间进程访问同一个驱动设备;
(2)SMP系统在不同的处理器上同时执行我们的代码;
(3)内核代码是可抢占的;
(4)设备中断是异步事件,也会导致代码的并发执行;
(5)内核提供的可延迟代码执行的机制,比如workqueue,tasklet,timer等等。
(6)热插拔机制,导致设备可能在我们正在使用时消失。


大部分竟态可通过使用内核的并发控制原语,并应用几个基本的原理来避免。
竟态通常作为对资源的共享访问结果而产生。
仔细编写的驱动代码应该具有最少的共享,这种思想的最明显应用就是避免使用全局变量。

共享是无法避免的,共享就是现实的生活。

访问管理常见技术称为”锁定“或者”互斥“:确保一次只有一个执行线程可操作共享资源。

信号量和互斥体

我们的目的是使对scull数据结构的操作是原子的,这意味着在涉及到其他执行进程之前,整个操作就已经结束了。
因此,我们必须建立临界区,在任意给定的时刻,代码只能被一个线程执行。

信号量

一个信号量本质上是一个整数值,它和一对函数联合使用,这一对函数通常称为P和V。
希望进入临界区的进程将在相关信号量上调用P函数,如果信号量的值大于零,则该值会减一,而进程可以继续;相反的情况,则调用V函数。

当信号量用于互斥时(即避免多个进程同时在一个临界区中运行),信号量的值应该初始化为1。这种信号量在任何给定的时刻只能由单个进程或线程拥有。

linux信号量的实现

信号量定义在 <asm/semaphore.h>中,相关类型是:struct semaphore。
声明和初始化方式:
(1)直接创建信号量,通过函数完成
    void sema_init(struct semaphore *sem, int val);

(2)声明互斥体
    DECLARE_MUTEX(name);                  // 称为name的信号量被初始化为1
    DECLARE_MUTEX_LOCKED(name);           // 称为name的信号量被初始化为0

(3)运行时初始化互斥体(动态分配互斥体)
    void init_MUTEX(struct semaphore *sem)
    void init_MUTEX_LOCKED(struct semaphore *sem)

在Linux世界中,P函数被称为down,”down“指的是该函数减小了信号量的值。它也许会将调用者置于休眠的状态,然后等待信号量变得可用,之后授予调用者对被保护资源的访问。

void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);

down:减小信号量的值,并在必要的时候一直等待。
down_interruptible:完成down相同的工作,但是操作时可以中断的,这是我们始终要使用的版本,它允许等待在某个信号量上的用户空间进程可被用户中断。
down_trylock:永远不会休眠;如果信号量在调用时不可获得,则立即会返回一个非零值。(是不是有点类似于非阻塞等待模式,只要访问,就要返回,不管有没有需要的信号量)


当互斥操作完成后,必须返回该信号量。linux等价于V的函数是up:

void up(struct semaphore *sem);

调用up后,调用者不再拥有该信号量。

在scull中使用信号量

在scull_dev结构体当中,定义了信号量,避免在访问该结构体时产生竟态现象。在访问(read,write等)获取这个结构体之前,必须获得该信号量。
scull_dev结构体如下:

  1. struct scull_dev {
  2.     struct scull_qset *data; /* Pointer to first quantum set */
  3.     int quantum; /* the current quantum size */
  4.     int qset; /* the current array size */
  5.     unsigned long size; /* amount of data stored here */
  6.     unsigned int access_key; /* used by sculluid and scullpriv */
  7.     struct semaphore sem; /* mutual exclusion semaphore */
  8.     struct cdev cdev;     /* Char device structure        */
  9. };

信号量使用步骤:
(1)定义信号量,结构体中已经定义好了;
(2)初始化信号量
  1. /* Initialize each device. */
  2.     for (i = 0; i < scull_nr_devs; i++) {
  3.         scull_devices[i].quantum = scull_quantum;
  4.         scull_devices[i].qset = scull_qset;
  5.         init_MUTEX(&scull_devices[i].sem);
  6.         scull_setup_cdev(&scull_devices[i], i);
  7.     }
(3)在访问scull_dev结构体资源的时候,先获取信号量
比如在scull_write函数中:
  1. if (down_interruptible(&dev->sem))   // 如果获取不到信号量,该调用进程将进入休眠,但是可以被中断,被其他的进程抢占CPU。
  2.         return -ERESTARTSYS;
代码对down_interruptible的返回值做检查;如果它返回非零值,则说明操作被中断;

(4)释放信号量
不管scull_write释放能够完成其他工作,它都必须释放信号量。
  1. out:
  2.     up(&dev->sem);
  3.     return retval;

读取者/写入者信号量

信号量对所有的调用者执行互斥,而不管每个线程到底做什么,但是,许多任务可以划分两种不同的工作类型:一些任务只需读取受保护的数据结构,而其他则必须做出修改。
允许多个并发的读取者是可能的,只要他们之中没有哪个要做修改。
这样做可以大大提高性能,因为只读任务可并行完成读取工作,而不需要等待其他读取者退出临界区。

Linux内核为这种情形提供了特殊的信号量类型,称为:rwsem,定义在<linux/rwsem.h>中,具体可以自己去阅读。

completion

completion接口:它允许一个线程告诉另一个线程某个工作已经完成,定义在<linux/completion.h>中。

a. 初始化completion函数

(1)利用内核提供的宏来静态定义和初始化completion

DECLARE_COMPLETION(my_completion);


(2)动态创建和初始化

struct completion my_completion;
init_completion(&my_completion);


b. 等待completion事件

调用函数:void wait_for_completion(struct completion * c);
该函数执行了一个非中断的等待,如果该函数被调用且没有人会完成该任务(等待这个信号量被完成),则将产生一个不可杀的进程。

c. 触发completion事件

void complete(struct completion * c);
void complete_all(struct completion * c);

complete只会唤醒一个等待线程;而complete_all允许唤醒所有等待线程。在大多数情况只有一个等待者,一次这两个函数产生相同的结果。

例子说明:在 ldd3/examples/misc-module/complete.c 中有如下代码:
  1. DECLARE_COMPLETION(comp);

  2. ssize_t complete_read (struct file *filp, char __user *buf, size_t count, loff_t *pos)
  3. {
  4.     printk(KERN_DEBUG "process %i (%s) going to sleep\n",
  5.             current->pid, current->comm);
  6.     wait_for_completion(&comp);   ///// 等待completion事件完成
  7.     printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm);
  8.     return 0; /* EOF */
  9. }

  10. ssize_t complete_write (struct file *filp, const char __user *buf, size_t count,
  11.         loff_t *pos)
  12. {
  13.     printk(KERN_DEBUG "process %i (%s) awakening the readers...\n",
  14.             current->pid, current->comm);
  15.     complete(&comp);                     /// 触发completion事件
  16.     return count; /* succeed, to avoid retrial */
  17. }





阅读(818) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~