对linux设备驱动中的并发控制相关内容的理解

来源:互联网 发布:安卓软件限速 编辑:程序博客网 时间:2024/06/05 06:05

这篇笔记主要是针对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()函数,

该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下:

int select(nfds, readfds, writefds, exceptfds, timeout);
nfds:select监视的文件句柄数,视进程中打开的文件数而定,一般设为你要监视各文件中的最大文件号加一。因为文件描述符set是静态创建的,它们对文件描述符的最大数目强加了一个限制,能够放进set中的最大文件描述符的值由FD_SETSIZE指定。在Linux中,这个值是1024。
readfds:select监视的可读文件句柄集合。
writefds: select监视的可写文件句柄集合。
exceptfds:select监视的异常文件句柄集合。
timeout:本次select()的超时结束时间。(见/usr/sys/select.h,可精确至百万分之一秒!)当前版本的Linux会自动修改timeout参数,设置它的值为剩余时间。因此,如果timeout被设置为5秒,然后在文件描述符准备好之前经过了3秒,则这一次调用select()返回时tv_sec将变为2。

这个参数有三种可能:

(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。

(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。

(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。


当readfds或writefds中映象的文件可读或可写或超时,本次select()就结束返回。程序员利用一组系统提供的宏在select()结束时便可判断哪一文件可读或可写,对Socket编程特别有用的就是readfds。

2. poll()函数

和select()不一样,poll()没有使用低效的三个基于位的文件描述符set,而是采用了一个单独的结构体pollfd数组,由fds指针指向这个组。pollfd结构体定义如下:

#include <sys/poll.h>
int poll(struct pollfd*fds,unsignedint 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写优先数据不会导致阻塞。POLLMSGSIGPOLL消息可用。此外,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代码的访问,保证这段代码不会被并行的运行

0 0