/* AUTHOR: Pinus
* Creat on : 2018-10-25
* KERNEL : linux-4.4.145
* BOARD : JZ2440(arm9 s3c2440)
* REFS : 韦东山视频教程第二期
*/
概述
(1)临界资源
在操作系统中,进程是占有资源的最小单位(线程可以访问其所在进程内的所有资源,但线程本身并不占有资源或仅仅占有一点必须资源)。但对于某些资源来说,其在同一时间只能被一个进程所占用。这些一次只能被一个进程所占用的资源就是所谓的临界资源。比如多台电脑都可以使用同一台打印机,但是,一个时刻只能有一台电脑来控制他进行打印,所以打印机在这里就是临界资源。
临界区:访问共享资源的代码区域。就是执行单元访问共享资源的那段代码就对啦。
并发:就是几个进程一起执行。
竞态:几个进程同时访问共享资源时发生
(2)同步、互斥
相交进程之间的关系主要有两种:同步与互斥(一定要记住:不是同步和异步)。同步是一种更为复杂的互斥,而互斥是一种特殊的同步。互斥是两个线程之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但他是必须要安照某种次序来运行相应的线程(也是一种互斥)!
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
(3)阻塞、非阻塞
首先来解释同步和异步的概念,这两个概念与消息的通知机制有关.
(一个别人的好例子很生动)举个例子,比如我去银行办理业务,可能选择排队等候,也可能取一个小纸条上面有我的号码,等到排到我这一号时由柜台的人通知我轮到我去办理业务了。前者(排队等候)就是同步等待消息,而后者(等待别人通知)就是异步等待消息。在异步消息处理中,等待消息者(在这个例子中就是等待办理业务的人)往往注册一个回调机制,在所等待的事件被触发时由触发机制(在这里是柜台的人)通过某种机制(在这里是写在小纸条上的号码)找到等待该事件的人。
而在实际的程序中,同步消息处理就好比简单的read/write操作,它们需要等待这两个操作成功才能返回;而异步处理机制就是类似于select/poll之类的多路复用IO操作,当所关注的消息被触发时,由消息触发机制通知触发对消息的处理。
其次再来解释一下阻塞和非阻塞,这两个概念与程序等待消息(无所谓同步或者异步)时的状态有关.
继续上面的那个例子,不论是排队还是使用号码等待通知,如果在这个等待的过程中,等待者除了等待消息之外不能做其它的事情,那么该机制就是阻塞的,表现在程序中,也就是该程序一直阻塞在该函数调用处不能继续往下执行。相反,有的人喜欢在银行办理这些业务的时候一边打打电话发发短信一边等待,这样的状态就是非阻塞的,因为他(等待者)没有阻塞在这个消息通知上,而是一边做自己的事情一边等待。但是需要注意了,第一种同步非阻塞形式实际上是效率低下的,想象一下你一边打着电话一边还需要抬头看到底队伍排到你了没有,如果把打电话和观察排队的位置看成是程序的两个操作的话,这个程序需要在这两种不同的行为之间来回的切换,效率可想而知是低下的。而后者,异步非阻塞形式却没有这样的问题,因为打电话是你(等待者)的事情,而通知你则是柜台(消息触发机制)的事情,程序没有在两种不同的操作中来回切换。
很多人会把同步和阻塞混淆,我想是因为很多时候同步操作会以阻塞的形式表现出来,比如很多人会写阻塞的read/write操作,但是别忘了可以对fd设置O_NONBLOCK标志位,这样就可以将同步操作变成非阻塞的了。同样的,很多人也会把异步和非阻塞混淆,因为异步操作一般都不会在真正的IO操作处被阻塞,比如如果用select函数,当select返回可读时再去read一般都不会被阻塞,就好比当你的号码排到时一般都是在你之前已经没有人了,所以你再去柜台办理业务就不会被阻塞。
同步和异步:上面提到过,同步和异步仅仅是关于所关注的消息如何通知的机制,而不是处理消息的机制。也就是说,同步的情况下,是由处理消息者自己去等待消息是否被触发,而异步的情况下是由触发机制来通知处理消息者,所以在异步机制中,处理消息者和触发机制之间就需要一个连接的桥梁,在我们举的例子中这个桥梁就是小纸条上面的号码,而在select/poll等IO多路复用机制中就是fd,当消息被触发时,触发机制通过fd找到处理该fd的处理函数。
请注意理解消息通知和处理消息这两个概念,这是理解这个问题的关键所在.还是回到上面的例子,轮到你办理业务这个就是你关注的消息,而去办理业务就是对这个消息的处理,两者是有区别的.而在真实的IO操作时,所关注的消息就是该fd是否可读写,而对消息的处理就是对这个fd进行读写.同步/异步仅仅关注的是如何通知消息,它们对如何处理消息并不关心,好比说,银行的人仅仅通知你轮到你办理业务了,而如何办理业务他们是不知道的。
而很多人之所以把同步和阻塞混淆,我想也是因为没有区分这两个概念,比如阻塞的read/write操作中,其实是把消息通知和处理消息结合在了一起,在这里所关注的消息就是fd是否可读/写,而处理消息则是对fd读/写。当我们将这个fd设置为非阻塞的时候,read/write操作就不会在等待消息通知这里阻塞,如果fd不可读/写则操作立即返回。
很多人又会问了,异步操作不会是阻塞的吧?已经通知了有消息可以处理了就一定不是阻塞的了吧?其实异步操作是可以被阻塞住的,只不过通常不是在处理消息时阻塞,而是在等待消息被触发时被阻塞.比如select函数,假如传入的最后一个timeout参数为NULL,那么如果所关注的事件没有一个被触发,程序就会一直阻塞在这个select调用处.而如果使用异步非阻塞的情况,比如aio_*组的操作,当我发起一个aio_read操作时,函数会马上返回不会被阻塞,当所关注的事件被触发时会调用之前注册的回调函数进行处理,具体可以参见我上面的连接给出的那篇文章.回到上面的例子中,如果在银行等待办理业务的人采用的是异步的方式去等待消息被触发,也就是领了一张小纸条,假如在这段时间里他不能离开银行做其它的事情,那么很显然,这个人被阻塞在了这个等待的操作上面;但是呢,这个人突然发觉自己烟瘾犯了,需要出去抽根烟,于是他告诉大堂经理说,排到我这个号码的时候麻烦到外面通知我一下(注册一个回调函数),那么他就没有被阻塞在这个等待的操作上面,自然这个就是异步+非阻塞的方式了。
实现
程序在前文基础上修改,只写出关键需要修改的地方
(1)原子操作
简单介绍一下原子变量(来源:百度百科)
一般我们在程序中修改一个代码的值会分为3步:读取==》 修改 ==》 回写。在允许多线程的程序中而对于某一资源我们只希望有一个线程操作它,我会想使用全局变量去作为标记变量。两个线程A、B,当A读取完该全局变量后正在修改它的值,而这时B进程正在读取该全局变量,对于B进程而言此全局变量的值和A进程是一样,这样A、B都可以正常进行,就与我们的意愿相违了,所以我们会想是不是可以定义一个变量,将对一个变量值得读取修改回写变成一个不可打断的操作,于是我们就有了原子变量。
互斥原理:设置原子变量初始化为1,当文件被打开,原子变量自减变为0,当有程序尝试再次打开文件时原子变量自减为负数,条件不满足,便无法成功打开,实现互斥。
static atomic_t canopen = ATOMIC_INIT(1); //定义原子变量canopen并初始化为1
static int buttons_drv_open(struct inode *inode, struct file *file)
{
if( !atomic_dec_and_test(&canopen) ){ //自减操作后测试其是否为0,为0则返回true,否则返回false。
atomic_inc(&canopen); //原子变量增加1
return -EBUSY;
}
/*BUTTONS GPF0,2 GPG3,11 */
/*配置为irq mode*/
request_irq(IRQ_EINT0, button_irq_handle, IRQF_TRIGGER_FALLING, "S2", &pins_desc[0]); //注册中断
...
}
int buttons_drv_close(struct inode *inode, struct file *file)
{
atomic_inc(&canopen); //原子变量增加1
//释放中断
free_irq(IRQ_EINT0, &pins_desc[0]);
...
}
(2)信号量:只有得到信号量的进程才能执行临界区的代码。得不到的进程进入休眠状态,直到有条件获取信号量,或者被其他进程终止。
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。信号量(semaphore)是非负整型变量,除了初始化之外,它只能通过两个标准原子操作:wait(semap) , signal(semap) ; 来进行访问;信号量通过一个计数器控制对共享资源的访问,信号量的值是一个非负整数,所有通过它的线程都会将该整数减一。如果计数器大于0,则访问被允许,计数器减1;如果为0,则访问被禁止,所有试图通过它的线程都将处于等待状态。
计数器计算的结果是允许访问共享资源的通行证。因此,为了访问共享资源,线程必须从信号量得到通行证, 如果该信号量的计数大于0,则此线程获得一个通行证,这将导致信号量的计数递减,否则,此线程将阻塞直到获得一个通行证为止。当此线程不再需要访问共享资源时,它释放该通行证,这导致信号量的计数递增,如果另一个线程等待通行证,则那个线程将在那时获得通行证。
Semaphore可以被抽象为五个操作:
1、 创建 Create
2、等待 Wait:线程等待信号量,如果值大于0,则获得,值减一;如果只等于0,则一直线程进入睡眠状态,知道信号量值大于0或者超时。 3、释放 Post:执行释放信号量,则值加一;如果此时有正在等待的线程,则唤醒该线程。
4、试图等待 TryWait:如果调用TryWait,线程并不真正的去获得信号量,还是检查信号量是否能够被获得,如果信号量值大于0,则TryWait返回成功;否则返回失败。
5、销毁 Destroy
信号量,是可以用来保护两个或多个关键代码段,这些关键代码段不能并发调用。在进入一个关键代码段之前,线程必须获取一个信号量。如果关键代码段中没有任何线程,那么线程会立即进入该框图中的那个部分。一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号量。
驱动程序的任务:
①定义信号量&初始化,通常使用下面的宏
DEFINE_SEMAPHORE(name); //定义一个名为name的信号量,并初始化为1
②获取信号量
//获得信号量sem,如果获取不到,这个函数会导致休眠,休眠 不可以被信号打断,即使是“kill -9 pid”也不能将他退出,不要在中断上下文中使用,因为它太固执了!
void down(struct semaphore * sem);
//进入睡眠状态的进程能被信号打断
void down_interruptible(struct semaphore * sem);
//尝试去获取信号量,成功返回0,失败返回非零,不会导致调用进程休眠,可用于中断上下文
void down_trylock(struct semaphore * sem);
“down”往下,让我想到:在资源上插一支旗子,获取他的人就把它拿下来,其他人就呵呵了^_^ (别人的话,很形象)
③释放信号量
int up(struct semaphore * sem); //把信号插回去,然后唤醒沉睡的人
(3)自旋锁:就是得不到自旋锁就“原地打转”,进程一直跑不过是原地循环
使用和信号量很相似,主要有:定义、初始化、获得、释放操作。
(4)中断屏蔽:让系统不能进行中断,这样就不会切换到其他进程来抢夺资源了。
使用方法:
①屏蔽中断
local_irq_disable();
②临界区安安心心地访问资源
③开中断
local_irq_enable();
信号量阻塞非阻塞实现代码实现:
static DEFINE_SEMAPHORE(button_lock); //定义信号量
static int buttons_drv_open(struct inode *inode, struct file *file)
{
/*获取信号量*/
if (file->f_flags & O_NONBLOCK){
down_trylock(&button_lock); //非阻塞
return -EBUSY;
}
else
down(&button_lock); //阻塞
/*BUTTONS GPF0,2 GPG3,11 */
/*配置为irq mode*/
request_irq(IRQ_EINT0, button_irq_handle, IRQF_TRIGGER_FALLING, "S2", &pins_desc[0]); //注册中断
...
return 0;
}
static ssize_t buttons_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
if (1 != size)
return -EINVAL;
if (file->f_flags & O_NONBLOCK){ //如果是非阻塞
if (!ev_press)
return -EAGAIN;
}
else{
/*
* 如果没有按键发生, 休眠
* 如果有,返回
*/
wait_event_interruptible(button_waitq, ev_press);
}
ev_press = 0;
copy_to_user(buf, &key_val, 1);
return 1;
}
int buttons_drv_close(struct inode *inode, struct file *file)
{
//释放中断
free_irq(IRQ_EINT0, &pins_desc[0]);
...
/*释放信号量*/
up(&button_lock);
return 0;
}
测试程序
fd = open("/dev/buttons", O_RDWR); // 阻塞方式运行
//fd = open("/dev/buttons", O_RDWR|O_NONBLOCK); // 非阻塞方式运行
if (fd < 0){
printf("can't open dev nod !\n");
return -1;
}