第十九章 驱动程序基石(一)-休眠-POLL-异步-阻塞

19.1休眠与唤醒         前面使用的是查询方式,会一直处于查询的状态,浪费CPU资源,引入休眠与唤醒,在按键没有按下的情况下,程序处于休眠状态,一旦按键按下,唤醒程序,处理中断。      ...

19.1休眠与唤醒

        前面使用的是查询方式,会一直处于查询的状态,浪费CPU资源,引入休眠与唤醒,在按键没有按下的情况下,程序处于休眠状态,一旦按键按下,唤醒程序,处理中断。
attachments-2020-06-2rzOP2965ee8033303b52.png

        怎么写出可以休眠和唤醒的程序呢?
        1、具有初始化队列;
        2、休眠函数wake_event_interrupt(wq, event);
        3、唤醒函数wake_up_interrupt(wq);

19.1.1 驱动程序
        看下老师写的文档就可以了,内核是如何执行的暂且过一下就好了,不用深究。
        02_read_key_irq函数中添加了字符设备的注册代码,作为字符设备来使用,前面简单的驱动中并没有去按照字符串设备使用。

        使用队列之前先对队列进行初始化,用一个宏进行初始化。
        程序比较简单,加了一个进入休眠的函数,和一个唤醒休眠的函数。
DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);      //声明一个waitqueue 

/* 实现对应的open/read/write等函数,file_operations结构体 */
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	//printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	int err;

	printk("g_key =  %x\n", g_key);
	wait_event_interruptible(gpio_key_wait, g_key); //g_key为0的话,休眠;为1时,唤醒;
	
	err = copy_to_user(buf, &g_key, 4);
	g_key = 0;
	
	return 4;
}

        简单追踪下wait_event_interruptible(),
/**
 * wait_event_interruptible - sleep until a condition gets true
 * @wq: the waitqueue to wait on  等待队列
 * @condition: a C expression for the event to wait for  唤醒条件
 *
 * The process is put to sleep (TASK_INTERRUPTIBLE) until the
 * @condition evaluates to true or a signal is received.  //条件为true才可以唤醒process
 * The @condition is checked each time the waitqueue @wq is woken up.
 *
 * wake_up() has to be called after changing any variable that could
 * change the result of the wait condition.
 *
 * The function will return -ERESTARTSYS if it was interrupted by a
 * signal and 0 if @condition evaluated to true.
 */
#define wait_event_interruptible(wq, condition)				\
({									\
	int __ret = 0;							\
	might_sleep();							\
	if (!(condition))						\
		__ret = __wait_event_interruptible(wq, condition);	\
	__ret;								\
})

wait_event_interruptible()
--->__wait_event_interruptible()
    --->___event()  //后面不用看了知道是休眠就ok了

        看下怎么唤醒?
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
	struct gpio_key *gpio_key = dev_id;
	int val;
	val = gpiod_get_value(gpio_key->gpiod);
	
	printk("key %d %d\n", gpio_key->gpio, val);
	printk("g_key =  %x\n", g_key);
	g_key = (gpio_key->gpio << 8) | val;  //按键放在高8位,按键值放在低8位;
	printk("g_key =  %x\n", g_key);
	
	wake_up_interruptible(&gpio_key_wait); //唤醒队列,g_key为true or signal
	
	return IRQ_HANDLED;
}
#define wake_up_interruptible(x)	__wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
/**
 * __wake_up - wake up threads blocked on a waitqueue.
 * @q: the waitqueue
 * @mode: which threads
 * @nr_exclusive: how many wake-one or wake-many threads to wake up
 * @key: is directly passed to the wakeup function
 */
void __wake_up(wait_queue_head_t *q, unsigned int mode,int nr_exclusive, void *key)
{
}

#define TASK_RUNNING		0
#define TASK_INTERRUPTIBLE	1 //可以被外部信号和内核唤醒
#define TASK_UNINTERRUPTIBLE	2 //不能由外部信号唤醒,只能由内核亲自唤醒。

        看下在板子上的运行情况(自己加了一点打印信息):
[root@imx6ull:/mnt]# insmod gpio_key_drv.ko
[root@imx6ull:/mnt]# [ 9457.591176]
[ 9457.591176]  key is 110, val is 0 //按下
[ 9457.596053]
[ 9457.596053]  g_key3 =  0x0
[ 9457.600285]
[ 9457.600285]  g_key4 =  0x6e00
[ 9463.661025]
[ 9463.661025]  key is 110, val is 1 //松开
[ 9463.665906]
[ 9463.665906]  g_key3 =  0x6e00
[ 9463.670400]
[ 9463.670400]  g_key4 =  0x6e01

^C
[root@imx6ull:/mnt]# ./button_test /dev/100ask_gpio_key
[ 9594.831307]
[ 9594.831307]  g_key1 =  0x8101 //读取最近一次按键变化
[ 9594.835854]
[ 9594.835854]  g_key2 =  0x8101
get button : 0x8101[ 9594.844391]
[ 9594.844391]  g_key1 =  0x0    //再去read时休眠

[ 9754.811863]
[ 9754.811863]  key is 110, val is 0 //按下,唤醒
[ 9754.816739]
[ 9754.816739]  g_key3 =  0x0
[ 9754.820977]
[ 9754.820977]  g_key4 =  0x6e00
[ 9754.825667]
[ 9754.825667]  g_key2 =  0x6e00
get button : 0x6e00[ 9754.834521]
[ 9754.834521]  g_key1 =  0x0     //再去read时休眠
        运行结果看着怪怪的,,,

        这版驱动程序有什么缺陷???多次按下按键的操作信息会被上次按下覆盖,因为按键值放在全局变量g_key中。解决,用一个缓冲区来保存按键值,使用环形缓冲区。不细究。

19.2 POLL机制
19.2.1 简介
        什么是POLL机制??? POLL机制的作用:使阻塞型函数超时返回,避免一直阻塞。就是定了一个闹钟,,,
    poll机制的大体流程:
    ① APP 不知道驱动程序中是否有数据,可以先调用 poll 函数查询一下, poll 函数可以传入超时时间
    ② APP 进入内核态,调用到驱动程序的 poll 函数,如果有数据的话立刻返回;
    ③ 如果发现没有数据时就休眠一段时间
    ④ 当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒 APP;
    ⑤ 当超时时间到了之后,内核也会唤醒 APP;
    ⑥ APP 根据 poll 函数的返回值就可以知道是否有数据,如果有数据就调用 read 得到数据。

        那应该有3种情况:
        1、APP来read,poll一下,有数据,直接返回;
        2、APP来read,poll一下,没数据,休眠,等待队列,有中断发生,APP读取数据返回;
        3、APP来read,没数据,休眠,等待队列,超时,poll一下,没数据,直接返回。

        poll主要做了2件事,将线程挂入到等待对队列(并未休眠),并返回读取状态。而休眠的工作由内核执行,也由内核唤醒。

19.2.2 代码编写
        驱动程序
        poll函数在ops结构体中啊!
#include <linux/poll.h>

static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	poll_wait(fp, &gpio_key_wait, wait);  //放入了等待队列但是还没有休眠
	return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}

/* 定义自己的file_operations结构体                                              */
static struct file_operations gpio_key_drv = {
	.owner	 = THIS_MODULE,
	.read    = gpio_key_drv_read,
	.poll    = gpio_key_drv_poll,
};
        只需要知道这样就是放入了poll等待队列就可以了,,,超时部分是由内核执行的。
        驱动程序中只需要添加一个poll函数就行了!!!
    
        看下事件的几种类型:
attachments-2020-06-YoAiE2rF5ee80536b0dc5.png

应用程序
        在调用 poll 函数时,要指明:
        ① 你要监测哪一个文件:哪一个 fd
        ② 你想监测这个文件的哪种事件:是 POLLIN、还是 POLLOUT
        最后,在 poll 函数返回时,要判断状态。

        POLL为什么是在Ubuntu中查找呢?应用程序所需函数。
book@100ask:~$ man poll
       #include 
       int poll(struct pollfd *fds, nfds_t nfds, int timeout);

DESCRIPTION
       The set of file descriptors to be monitored is specified in the fds argument, which is an array of structures of the
       following form:
           struct pollfd {
               int   fd;         /* file descriptor 文件描述符 */
               short events;     /* requested events 请求事件*/
               short revents;    /* returned events 返回事件*/
           };
       The caller should specify the number of items in the fds array in nfds.
struct pollfd fds[1];  //定义1个pollfd结构体,nfds = 1
int timeout_ms = 5000;
int ret;

fds[0].fd = fd;         //初始化fd
fds[0].events = POLLIN; //初始化fd,有数据可读

ret = poll(fds, 1, timeout_ms);  
if ((ret == 1) && (fds[0].revents & POLLIN)) //有数据且可读
{
    read(fd, &val, 4);
    printf("get button : 0x%x\n", val);
}
else
{
    printf("timeout\n");
}
[root@imx6ull:/mnt]# ./button_test /dev/100ask_gpio_key
[ 5826.037964] gpio_key_drv_poll line 96  //第一次poll
/* wait 5s ,从行首的时间值也能看出来时等了5s */
[ 5831.042347] gpio_key_drv_poll line 96 //超时,唤醒,再去poll
timeout!!! //提醒已超时

[ 5831.053802] gpio_key_drv_poll line 96 //再次poll
/* wait 5s ,从行首的时间值也能看出来时等了5s */
[ 5836.059028] gpio_key_drv_poll line 96 //超时,唤醒,再去poll
timeout!!!

[ 5846.096934] gpio_key_drv_poll line 96 //N次poll

[ 5846.577784] key 129 0    //按键被按下,唤醒poll
[ 5846.581256] gpio_key_drv_poll line 96 //N+1次poll
get button : 0x8100     //读取按键数据

[ 5846.590884] gpio_key_drv_poll line 96

[ 5846.836500] key 129 1
[ 5846.840000] gpio_key_drv_poll line 96
get button : 0x8101
[ 5846.849249] gpio_key_drv_poll line 96
        清晰了……

19.3 异步通知
19.3.1简介
        读取按键的几种方式:
        查询: 耗资源;
        中断: 一直读取;
        poll: 指定超时时间;
        共同特点:应用程序主动读取按键;
        异步通知:有按键按下,触发应用程序去读取按键;

步骤:
    1、APP注册signal(SIGIO,func);
    2、APP先打开驱动程序,得到fd;
    3、APP把自己的进程号PID告诉驱动程序;fcntl(fs, F_SETOWN, getpid())并没有给到驱动程序,而是给了内核文件系统中sys_fcntl()的filp结构体;
    4、APP获取驱动程序的操作标志位flag,flags = fcntl(fs, F_GETFL);
    4、APP设置驱动程序的flag为FASYNC,启动异步通知机制;fcntl(fd,F_SETL, flags | FASYNC);
    5、驱动程序会调用fasync_helper(fd, filp, on, &button_async)来启动异步通知机制;当oflag中的FASYNC为1时,会设置on为1,并分配一个button_async结构体,并把filp(PID)传递给button_async。如果关闭FASYNC,把oflag中的   FASYNC为0时,on会被设置为0,button_async被赋值为NULL;(过程好繁琐)
    6、设置好一切之后,APP做自己的事情;
    7、当按键被按下时会调用kill_fasync(&button_async, SIGIO, POLL_IN),从button_async_fa_file中取出PID,向它发送信号;
    8、APP接收到驱动程序传递过来的信号,会去调用注册的func函数,可以调用read函数,读取按键数据。
这过程为什么搞得这么复杂啊!!!

attachments-2020-06-5OkQd9Iw5ee80597d60c2.png
老师讲课笔记:
重点从②开始:
② APP 给 SIGIO 这个信号注册信号处理函数 func,以后 APP 收到 SIGIO 信号时,这个函数会被自动调
用;
③ 把 APP 的 PID(进程 ID)告诉驱动程序,这个调用不涉及驱动程序,在内核的文件系统层次记录 PID;
④ 读取驱动程序文件 Flag;
⑤ 设置 Flag 里面的 FASYNC 位为 1:当 FASYNC 位发生变化时,会导致驱动程序的 fasync 被调用;
⑥⑦ 调用 faync_helper,它会根据 FAYSNC 的值决定是否设置 button_async->fa_file=驱动文件 filp:
驱动文件 filp 结构体里面含有之前设置的 PID。
⑧ APP 可以做其他事;
⑨⑩ 按下按键,发生中断,驱动程序的中断服务程序被调用,里面调用 kill_fasync 发信号;
⑪⑫⑬ APP 收到信号后,它的信号处理函数被自动调用,可以在里面调用 read 函数读取按键。

19.3.2编程
        按照上面的流程来看下用到的函数:

1、signal()
SYNOPSIS
       #include <signal.h>

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);
RETURN VALUE
       signal()  returns the previous value of the signal handler, or SIG_ERR on error.  
        依次定义自己的signal()和sighandler_t(),
signal(SIGIO, sig_func);
        看下SIGIO的定义,在文件文件include\uapi\asm-generic\signal.h中:
#define SIGBUS		 7
#define SIGFPE		 8
#define SIGKILL		 9
#define SIGUSR1		10
#define SIGSEGV		11
#define SIGIO		29
#define SIGPOLL		SIGIO
        自定义的APP func函数,读取按键值。
static void sig_func(int sig)
{
	int val;
	read(fd, &val, 4);  //读按键值
	printf("get button : 0x%x\n", val);
}
2、fcntl()
        fcntl是file control的缩写吗?对文件的控制?“fcntl是计算机中的一种函数,通过fcntl可以改变已打开的文件性质。”参考:https://www.cnblogs.com/xuyh/p/3273082.html
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);         
int fcntl(int fd, int cmd, struct flock *lock);
        根据cmd的不同,fcntl实现不同的功能:
        1.复制一个现有的描述符(cmd=F_DUPFD).
        2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
        3.获得/设置文件状态标记(cmd=F_GETFLF_SETFL).
        4.获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
        5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).

fcntl(fd, F_SETOWN, getpid());  //获取当前APP的PID
flags = fcntl(fd, F_GETFL);     //获取当前APP的状态
fcntl(fd, F_SETFL, flags | FASYNC); //设置当前APP的状态,通知驱动程序启用FASYNC

while (1) //APP等待驱动程序通知
{
    printf("www.100ask.net \n");
    sleep(2);
}

知识点:文件标志位
        打开文件include\uapi\asm-generic\fcntl.h:
#define O_RDONLY	00000000   //只读
#define O_WRONLY	00000001   //只写
#define O_RDWR	00000002   //可读可写
#ifndef FASYNC
#define FASYNC	00020000   /* fcntl */
#endif
3、gpio_key_drv_fasync()
        上面2个函数定义在APP中,看下驱动呢?
        fasync()还是在ops结构体中啊,好吧,
/* 定义自己的file_operations结构体                                              */
static struct file_operations gpio_key_drv = {
	.owner	 = THIS_MODULE,
	.read    = gpio_key_drv_read,
	.poll    = gpio_key_drv_poll,
	.fasync  = gpio_key_drv_fasync,
};

struct fasync_struct *button_fasync; 

/* 谁来调用它?*/
static int gpio_key_drv_fasync(int fd, struct file *file, int on)
{
    /* 申请并启动fasync */
	if (fasync_helper(fd, file, on, &button_fasync) >= 0) 
		return 0;
	else
		return -EIO;
}

        先看下fasync_struct:
struct fasync_struct {
	spinlock_t		fa_lock;
	int			magic;
	int			fa_fd; //晓得
	struct fasync_struct	*fa_next; /* singly linked list */
	struct file		*fa_file; //晓得
	struct rcu_head		fa_rcu;
};
        简单追踪下fasync_helper(),感觉就是为驱动程序申请并初始化一个fasync_struct。
fasync_helper(fd, file, on, &button_fasync)
--->fasync_add_entry(fd, filp, fapp);  fapp = button_fasync
    --->new = fasync_alloc();
    --->fasync_insert_entry(fd, filp, fapp, new)
        --->new->fa_file = filp;
        --->new->fa_fd = fd;
        --->filp->f_flags |= FASYNC;
4、kill_fasync()
        知识点2:在用户空间使用kill函数给进程发信号,比如杀死一个进程使用kill -9;在内核空间使用kill_fasync函数给用户空间的进程发送信号;
kill_fasync()定义在中断处理函数中,只有中断发送了,才会去告诉APP。
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    /* 告诉APP发生了SIGIO中断,有数据可以读取 */
	kill_fasync(&button_fasync, SIGIO, POLL_IN);
}

        

每个函数倒是不难,就是这个机制有点繁琐哈。


19.4 阻塞与非阻塞
        摘录老师讲课笔记:
所谓阻塞,就是等待某件事情发生。比如调用 read 读取按键时,如果没有按键数据则 read 函数不会返回,它会让线程休眠等待。
使用 poll 时,如果传入的超时时间不为 0,这种访问方法也是阻塞的。
使用 poll 时,可以设置超时时间为 0,这样即使没有数据它也会立刻返回,这就是非阻塞方式。能不能让 read 函数既能工作于阻塞方式,也可以工作于非阻塞方式? 可以
APP 调用 open 函数时,传入 O_NONBLOCK,就表示要使用非阻塞方式;默认是阻塞方式。
注意:对于普通文件、块设备文件, O_NONBLOCK 不起作用。
注意:对于字符设备文件, O_NONBLOCK 起作用的前提是驱动程序针对 O_NONBLOCK 做了处理。
        阻塞和非阻塞需要设置file的f_flags:
//include\uapi\asm-generic\fcntl.h
#ifndef O_NONBLOCK
#define O_NONBLOCK	00004000
#endif
        
        APP中先用非阻塞的方式读取文件,然后再用fcntl 修改为阻塞方式。
/* 2. 打开文件 */
	fd = open(argv[1], O_RDWR | O_NONBLOCK);  //以非阻塞的方式read,有无数据都立刻返回

	for (i = 0; i < 5; i++) 
	{
		if (read(fd, &val, 4) == 4) 
			printf("get button: 0x%x\n", val); //按键按下
		else
			printf("get button: -1\n");  //按键未按下
	}

	flags = fcntl(fd, F_GETFL);   //获取flags
	fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);  //修改flags为阻塞方式

	while (1)
	{
		if (read(fd, &val, 4) == 4)
			printf("get button: 0x%x\n", val); //按键按下
		else
			printf("while get button: -1\n");
	}

        对应的驱动程序应该怎么改呢?前面的poll是按键没有按下时等待,按键按下返回或超时返回。这是阻塞的方式,非阻塞就是没有按键按下,也立刻返回,有按键按下最好了。
        修改read(),添加判断条件,如果环形缓冲区内没有数据,且flags为O_NONBLOCK,则立马返回,告知APP,没有数据。
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	//printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	int err;
	int key;
    /*判断缓冲区是否有数据?无数据为1,有数据为0;f_flags 是否为O_NONBLOCK?*/
	if (is_key_buf_empty() && (file->f_flags & O_NONBLOCK))
		return -EAGAIN;
	
	wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
	key = get_key();
	err = copy_to_user(buf, &key, 4);
	
	return 4;
}
        上机实验:
        加一些打印信息
[root@imx6ull:/mnt]# [21109.063750] is_key_buf_empty() is 1?
[21109.067887] file->f_flags is 0x802? //‭ 0011 0010 0010‬ 非阻塞
get button: -1

[21109.072771] is_key_buf_empty() is 1?
[21109.079971] file->f_flags is 0x802?
get button: -1
…………
[21109.083778] is_key_buf_empty() is 1?
[21109.157050] file->f_flags is 0x802?   
get button: -1

[21109.160784] is_key_buf_empty() is 1?
[21109.166514] file->f_flags is 0x2?  //‭0010 阻塞‬
[21264.847147] key 129 0
get button: 0x8100[21264.849901] is_key_buf_empty() is 1?

[21264.855073] file->f_flags is 0x2?
[21265.057701] key 129 1
get button: 0x8101[21265.061582] is_key_buf_empty() is 1?

[21265.066754] file->f_flags is 0x2?

纯净版
[root@imx6ull:/mnt]# get button1: -1  //非阻塞读取数据
get button1: -1 //非阻塞读取数据
get button1: -1 //非阻塞读取数据
get button1: -1 //非阻塞读取数据
get button1: -1 //非阻塞读取数据
[22293.778072] key 129 0 //阻塞读取数据,有按键按下
get button: 0x8100
[22293.975601] key 129 1 //阻塞读取数据,有按键按下
get button: 0x8101
[22300.841092] key 110 0 //阻塞读取数据,有按键按下
get button: 0x6e00
[22301.048064] key 110 1 //阻塞读取数据,有按键按下
get button: 0x6e01
        APP中并没有用到poll和fasync,那驱动程序里面是不是可以将gpio_key_drv_poll()和gpio_key_drv_fasync()删掉啊,不行!
        驱动程序程序“只提供功能,不提供策略”。就是说驱动程序可以提供休眠唤醒、查询等等各种方式,驱动程序只提供这些能力,怎么用由 APP 决定。

        下一节是定时器,内容很多……

你可能感兴趣的文章

相关问题

0 条评论&回复

请先 登录 后评论
zxq
zxq

11 篇文章

作家榜 »

  1. 百问网-周老师 19 文章
  2. st_ashang 14 文章
  3. 渐进 12 文章
  4. zxq 11 文章
  5. CMOneMO 8 文章
  6. helloworld 8 文章
  7. Infinite 5 文章
  8. 谢工 5 文章