这篇笔记主要是针对linux设备驱动中的并发控制内容的学习后,存在的一些问题的补充学习和调查结果,路过的大神们也可以帮我看看理解的是否正确,有问题的话欢迎大家帮我指出来,小弟在此谢过啦!
问题一 什么是死锁,什么情况下会发生死锁?
回答:
1. 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
换一种说法:
死锁是因为多线程访问共享资源,由于访问的顺序不当所造成的,通常是一个线程锁定了一个资源A,而又想去锁定资源B;在另一个线程中,锁定了资源B,而又想去锁定资源A以完成自身的操作,两个线程都想得到对方的资源,而不愿释放自己的资源,造成两个线程都在等待,而无法执行的情况。
2. 产生条件
虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件。
1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
对于死锁的理解找到一篇比较好的博文,有兴趣的可以看一下:
http://www.360doc.com/content/11/0904/13/834759_145686705.shtml
问题二 poll()和select()的区别,
1. 先理解select()函数,
该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下:
这个参数有三种可能:
(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。
(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。
2. poll()函数
和select()不一样,poll()没有使用低效的三个基于位的文件描述符set,而是采用了一个单独的结构体pollfd数组,由fds指针指向这个组。pollfd结构体定义如下:
int poll(struct pollfd*fds,unsigned int nfds,int timeout);
struct pollfd {
int fd; /* file descriptor */
short events;/* requested events to watch */
short revents;/* returned events witnessed */
};
每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码。内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。
合法的事件如下: | |
POLLIN | 有数据可读。 |
POLLRDNORM | 有普通数据可读。 |
POLLRDBAND | 有优先数据可读。 |
POLLPRI | 有紧迫数据可读。 |
POLLOUT | 写数据不会导致阻塞。 |
POLLWRNORM | 写普通数据不会导致阻塞。 |
POLLWRBAND | 写优先数据不会导致阻塞。 |
POLLMSG | SIGPOLL消息可用。 |
此外,revents域中还可能返回下列事件: | |
POLLER | 指定的文件描述符发生错误。 |
POLLHUP | 指定的文件描述符挂起事件。 |
POLLNVAL | 指定的文件描述符非法。 |
timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。
3. epoll()
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll操作过程需要三个接口,分别如下:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
详细的在Anker的博客中有相关说明:
http://www.cnblogs.com/Anker/archive/2013/08/17/3263780.html
问题三 对wait_queue的理解。
1. 经过一些调查,认为等待队列实际就是在进程中追加一个等待队列,先用当前进程生成了一个wait_queue_t,把当前进程的state改成TASK_INTERRUPTIBLE,这时调用shedule(),将当前进程放到scheduling queue 中,等待队列被唤醒是,挑出来继续执行。执行完 schedule() 之后,当前进程就没办法继续执行了,一直阻塞。而当以后等待队列等待的条件满足时,当前进程被 wake up 时,就会从 schedule() 之后开始执行。并将当前进程的状态设定为RUNNING.
用网上整理的处理步骤:
A)用当前的进程描述块(PCB)初始化一个wait_queue描述的等待任务。
B)在等待队列锁资源的保护下,将等待任务加入等待队列。
C)判断等待条件是否满足,如果满足,那么将等待任务从队列中移出,退出函数。
D)如果条件不满足,那么任务调度,将CPU资源交与其它任务。
E)当睡眠任务被唤醒之后,需要重复(2)、(3)步骤,如果确认条件满足,退出等待事件函数。
2. 等待队列接口:
wait_queue_head_t wait_que --> 声明一个等待队列头
init_waitqueue_head( &wait_que) --> 初始化等待队列头
另一个方法:DECLARE_WAITQUEUE(name, tsk) --> 使用等待队列时首先需要定义一个wait_queue_head,这可以通过DECLARE_WAIT_QUEUE_HEAD宏来完成,这是静态定义的方法。该宏会定义一个wait_queue_head,并且初始化结构中的锁以及等待队列
DECLARE_WAITQUEUE(name, tsk) --> 声明一个等待队列并初始化为name
wait_event(wq, condition) --> 这是一个宏,让当前任务处于等待事件状态。输入参数如下:
wq:等待队列
conditions:等待条件
wait_event_timeout(wq, condition, timeout) --> 功能与wait_event类似,多了一个超时机制。参数中多了一项超时时间。
wait_event_interruptible(wq, condition) --> 这是一个宏,与前两个宏相比,该宏定义的等待能够被消息唤醒。如果被消息唤醒,那么返回- ERESTARTSYS。输入参数如下:
wq:等待队列
condition:等待条件
rt:返回值
wait_event_interruptible_timeout(wq, condition, timeout) --> 与上一个相比,多了超时机制
wake_up(x) --> 唤醒等待队列中的一个任务
wake_up_interruptible(x) --> 用于唤醒wake_event_interruptible()睡眠的进程
wake_up_all(x) --> 唤醒等待队列中的所有任务
3. Linux将进程状态描述为如下五种:
TASK_RUNNING:可运行状态。处于该状态的进程可以被调度执行而成为当前进程。
TASK_INTERRUPTIBLE:可中断的睡眠状态。处于该状态的进程在所需资源有效时被唤醒,也可以通过信号或定时中断唤醒(因为有signal_pending()函数)。
TASK_UNINTERRUPTIBLE:不可中断的睡眠状态。处于该状态的进程仅当所需资源有效时被唤醒。
TASK_ZOMBIE:僵尸状态。表示进程结束且已释放资源,但其task_struct仍未释放。
TASK_STOPPED:暂停状态。处于该状态的进程通过其他进程的信号才能被唤醒。
问题四 Semaphore, Mutex , Completion的区别
信号量(sema) | 完成量(completion) | 互斥体(mutex) | |
1.定义 | struct semaphore sem; | struct completion my_completion; | struct mutex my_mutex; |
2.初始化 | void sema_init (struct semaphore *sem, int val) | init_completion(&my_completion); | mutex_init(&my_mutex); |
3. 定义并初始化 | DECLARE_MUTEX(name) | DECLARE_COMPLETION(my_completion); | - |
DECLARE_MUTEX_LOCKED(name) | |||
4.获得/等待 | void down(struct semaphore * sem); | void wait_for_completion(struct completion *c); | void fastcall mutex_lock(struct mutex *lock); |
int down_interruptible(struct semaphore * sem); | int fastcall mutex_lock_interruptible(struct mutex *lock); | ||
int down_trylock(struct semaphore * sem); | int fastcall mutex_trylock(struct mutex *lock); | ||
5.释放/唤醒 | void up(struct semaphore * sem); | void complete(struct completion *c); | void fastcall mutex_unlock(struct mutex *lock); |
void complete_all(struct completion *c); | |||
说明 | 与自旋锁不同的是,当获取不到信号量时,进程不会原地打转,而是进入休眠等待状态。 | 前者只唤醒一个等待的执行单元, | mutex_lock前者引起的睡眠不能被信号打断,而后者mutex_lock_interruptible可以。 |
1. 信号量被初始化为1,一般用于互斥的信号量,保护保护临界区 | 后者释放所有等待同一完成量的执行单元。 | mutex_trylock()用于尝试获得 mutex,获取不到 mutex 时不会引起进程睡眠。 | |
2. 如果信号量被初始化为 0,则它可以用于同步,同步意味着一个执行单元的继续 执行需等待另一执行单元完成某事,保证执行的先后顺序。信号灯其实就是一个计数器,也是一个整数。每一次调用wait操作将会使semaphore值减一,而如果semaphore值已经为0,则wait操作将会阻塞。每一次调用post操作将会使semaphore值加一。将这些操作用到上面的问题中。工作线程每一次调用wait操作,如果此时链表中没有节点,则工作线程将会阻塞,直到链表中有节点。生产者线程在每次往链表中添加节点后调用post操作,信号灯值会加一。这样阻塞的工作线程就会停止阻塞,继续往下执行。 | 一般信号量的的处理会限制在一个函数内,但是有时会函数A的处理的前提条件是函数B,A必须等待B处理后才能继续,可以用信号量来进行处理,但linux kernel提供complete的方式。使用方式如下: •头文件#include ,数据结构为struct completion ,初始化为init_completion(struct completion * comp ) ,也可以直接使用DECLARE_COMPLETION( comp ); •在A函数中,如果需要等待其他的处理,使用void wait_for_completion(struct completion * comp ); 则在这个位置上将处于非中断的sleep,进行等待,也就是相关的线程/进程,用户是无法kill的。 •在B函数,如果已经处理完,可以交由A函数处理,有下面两种方式 •void complete(struct completion * comp ); 如果要执行A必须等待B先执行,B执行后,A可以继续执行。如果A需要再次执行,则需要确保下一次B执行完。如果连续执行两次B,则可以执行两次A,第三次A要等第三次B执行完。 •void complete_all(struct completion * comp ); 只要B执行完,A就可以执行,无论执行多少次。如果需要再等待B的直系个可以使用INIT_COMPLETION(struct completion * comp ) 。重新初始化completion即可。 •void complete_and_exit(struct completion * comp ,long retval ) ; 这个处理具有complete的功能外,还将调用它的线程/进程终止。可用于一些无限循环的场景,例如受到某个cleaned up的信息后,e通知用户程序终止,允许A函数执行。 | 而一个task在拿到mutex之后释放之前不宜进行太长时间的操作,更不能阻塞。 | |
理解 | 信号量是一个可以容纳N人的房间,如果人不满就可以进去,如果人满了,就要等待有人出来。对于N=1的情况,称为binary semaphore。一般的用法是,用于限制对于某一资源的同时访问,此种情况下,可以将binary semaphore(二进制信号量)理解为mutex,实现对临界区的保护。 Class semaphore { public: Semaphore(int count, int max_count); ~Semaphore(); void Unsignal();//等待操作P,count--,如果count==0则等待 void Signal();//释放操作V,count++ } | completion是类似于信号量的东西,在线程之间同步时,可以将completion理解为信号量的一种简单实现模式 Classmutex { public: waitMutex();//阻塞线程,直到其它线程释放互斥锁 releaseMutex();//释放线程 } | Mutex是一把钥匙,一个人拿了就可进入一个房间,出来的时候把钥匙交给队列的第一个。一般的用法是用于串行化对critical section代码的访问,保证这段代码不会被并行的运行 |