正点原子 发表于 2022-1-22 15:53:33

《领航者ZYNQ之嵌入式Linux开发指南_V2.0》第三十四章 Linux IO实验

1)实验平台:正点原子领航者V2 ZYNQ开发板
2)章节摘自【正点原子】《领航者ZYNQ之嵌入式Linux开发指南_V2.0》
3)购买链接:https://detail.tmall.com/item.htm?id=609032204975
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-329957-1-1.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子FPGA技术交流QQ群:90562473








第三十四章 Linux阻塞和非阻塞IO实验
       阻塞和非阻塞IO是Linux驱动开发里面很常见的两种设备访问模式,在编写驱动的时候一定要考虑到阻塞和非阻塞。本章我们就来学习一下阻塞和非阻塞IO,以及如何在驱动程序中处理阻塞与非阻塞,如何在驱动程序使用等待队列和poll机制。

       1.1阻塞和非阻塞IO
1.1.1阻塞和非阻塞简介
       这里的“IO”并不是我们学习STM32或者其他单片机的时候所说的“GPIO”(也就是引脚)。这里的IO指的是Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作。当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式IO就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。阻塞式IO如图 34.1.1所示:

图 34.1.1 阻塞IO访问示意图
       图 34.1.1中应用程序调用read函数从设备中读取数据,当设备不可用或数据未准备好的时候就会进入到休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给应用程序。非阻塞IO如图 34.1.2所示:

图 34.1.2 非阻塞IO访问示意图
       从图 34.1.2可以看出,应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,这样一直往复循环,直到数据读取成功。
       应用程序可以使用如下所示示例代码来实现阻塞访问:
示例代码 34.1.1 应用程序阻塞读取数据
1 int fd;
2 int data = 0;
3
4 fd = open("/dev/xxx_dev", O_RDWR);      /* 阻塞方式打开 */
5 ret = read(fd, &data, sizeof(data));                /* 读取数据 */       从示例代码 34.1.1可以看出,对于设备驱动文件的默认读取方式就是阻塞式的,所以我们前面所有的例程测试APP都是采用阻塞IO。
       如果应用程序要采用非阻塞的方式来访问驱动设备文件,可以使用如下所示代码:
示例代码 34.1.2 应用程序非阻塞读取数据
1 int fd;
2 int data = 0;
3
4 fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK);      /* 非阻塞方式打开 */
5 ret = read(fd, &data, sizeof(data));                                                /* 读取数据 */       第4行使用open函数打开“/dev/xxx_dev”设备文件的时候添加了参数“O_NONBLOCK”,表示以非阻塞方式打开设备,这样从设备中读取数据的时候就是非阻塞方式的了。
1.1.2等待队列
       1、等待队列头
       阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。Linux内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作,如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体wait_queue_head_t表示,wait_queue_head_t结构体定义内核源码目录include/linux/wait.h头文件中,结构体内容如下所示:
示例代码 34.1.3 wait_queue_head_t结构体
34 struct wait_queue_head {
35spinlock_t                lock;
36struct list_head      head;
37 };
38 typedef struct wait_queue_head wait_queue_head_t;       定义好等待队列头以后需要初始化,使用init_waitqueue_head初始化等待队列头,init_waitqueue_head其实是一个宏定义,也是定义在include/linux/wait.h头文件中,原型如下:
#define init_waitqueue_head(wq_head)                        \
      do {                        \
                static struct lock_class_key __key;                              \
                                                                              \
                __init_waitqueue_head((wq_head), #wq_head, &__key);                \
      } while (0)       参数wq_head就是要初始化的等待队列头。
       也可以使用宏DECLARE_WAIT_QUEUE_HEAD来一次性完成等待队列头的定义和初始化。
       2、等待队列项
       等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。结构体wait_queue_entry_t表示等待队列项,该结构体内容如下:
示例代码 34.1.4 wait_queue_entry_t结构体
14 typedef struct wait_queue_entry wait_queue_entry_t;
......
24 /*
25* A single wait-queue entry structure:
26*/
27 struct wait_queue_entry {
28   unsigned int                flags;
29   void                              *private;
30   wait_queue_func_t      func;
31   struct list_head                entry;
32};       使用宏DECLARE_WAITQUEUE定义并初始化一个等待队列项,宏的内容如下:
#define DECLARE_WAITQUEUE(name, tsk)                \
       struct wait_queue_entry name = __WAITQUEUE_INITIALIZER(name, tsk)       name就是等待队列项的名字,tsk表示这个等待队列项属于哪个任务(进程),一般设置为current,在Linux内核中current相当于一个全局变量,表示当前进程。因此宏DECLARE_WAITQUEUE就是给当前正在运行的进程创建并初始化了一个等待队列项。
       3、将队列项添加/移除等待队列头
       当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中,只有添加到等待队列头中以后进程才能进入休眠态。当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可,等待队列项添加API函数如下:
void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry);      函数参数和返回值含义如下:
       wq_head:等待队列项要加入的等待队列头。
       wq_entry:要加入的等待队列项。
       返回值:无。
       等待队列项移除API函数如下:
void remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry);       函数参数和返回值含义如下:
       wq_head:要删除的等待队列项所处的等待队列头。
       wq_entry:要删除的等待队列项。
       返回值:无。
       4、等待唤醒
       当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下:
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)       参数q就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。wake_up函数可以唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程,而wake_up_interruptible函数只能唤醒处于TASK_INTERRUPTIBLE状态的进程。
       5、等待事件
       除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程,和等待事件有关的API函数如表 34.1.1所示:

表 34.1.1 等待事件API函数
1.1.3轮询
       如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,也就是轮询。poll、epoll和select可以用于处理轮询,应用程序通过select、epoll或poll函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用select、epoll或poll函数的时候设备驱动程序中的poll函数就会执行,因此需要在设备驱动程序中编写poll函数。我们先来看一下应用程序中使用的select、poll和epoll这三个函数。
       1、select函数
       select函数原型如下:
int select(int      nfds, fd_set      *readfds, fd_set      *writefds, fd_set      *exceptfds, struct timeval      *timeout)       函数参数和返回值含义如下:
       nfds:要操作的文件描述符个数。
       readfds、writefds和exceptfds:这三个指针指向描述符集合,这三个参数指明了关心哪些描述符、需要满足哪些条件等等,这三个参数都是fd_set类型的,fd_set类型变量的每一个位都代表了一个文件描述符。readfds用于监视指定描述符集的读变化,也就是监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取那么seclect就会返回一个大于0的值表示文件可以读取。如果没有文件可以读取,那么就会根据timeout参数来判断是否超时。可以将readfs设置为NULL,表示不关心任何文件的读变化。writefds和readfs类似,只是writefs用于监视这些文件是否可以进行写操作。exceptfds用于监视这些文件的异常。
      比如我们现在要从一个设备文件中读取数据,那么就可以定义一个fd_set变量,这个变量要传递给参数readfds。当我们定义好一个fd_set变量以后可以使用如下所示几个宏进行操作:
void FD_ZERO(fd_set *set)
void FD_SET(int fd, fd_set *set)
void FD_CLR(int fd, fd_set *set)
intFD_ISSET(int fd, fd_set *set)       FD_ZERO用于将fd_set变量的所有位都清零,FD_SET用于将fd_set变量的某个位置1,也就是向fd_set添加一个文件描述符,参数fd就是要加入的文件描述符。FD_CLR用户将fd_set变量的某个位清零,也就是将一个文件描述符从fd_set中删除,参数fd就是要删除的文件描述符。FD_ISSET用于测试fd_set的某个位是否置1,也就是判断某个文件是否可以进行操作,参数fd就是要判断的文件描述符。
       timeout:超时时间,当我们调用select函数等待某些文件描述符可以设置超时时间,超时时间使用结构体timeval表示,结构体定义如下所示:
struct timeval {
long    tv_sec;                /* 秒 */
long    tv_usec;                /* 微妙 */
};       当timeout为NULL的时候就表示无限期的等待。
       返回值:0,表示的话就表示超时发生,但是没有任何文件描述符可以进行操作;-1,发生错误;其他值,可以进行操作的文件描述符个数。
       使用select函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:
示例代码 34.1.5 select函数非阻塞读访问示例
1 void main(void)
2 {
3   int ret, fd;                        /* 要监视的文件描述符 */
4   fd_set readfds;                /* 读操作文件描述符集 */
5   struct timeval timeout;      /* 超时结构体 */
6
7   fd = open("dev_xxx", O_RDWR | O_NONBLOCK);      /* 非阻塞式访问 */
8
9   FD_ZERO(&readfds);                /* 清除readfds */
10   FD_SET(fd, &readfds);                /* 将fd添加到readfds里面 */
11
12   /* 构造超时时间 */
13   timeout.tv_sec = 0;
14   timeout.tv_usec = 500000;      /* 500ms */
15
16   ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
17   switch (ret) {
18   case 0:                        /* 超时 */
19         printf("timeout!\r\n");
20         break;
21   case -1:                        /* 错误 */
22         printf("error!\r\n");
23         break;
24   default:                        /* 可以读取数据 */
25         if(FD_ISSET(fd, &readfds)) {                /* 判断是否为fd文件描述符 */
26                                                                     /* 使用read函数读取数据 */
27         }
28         break;
29   }
30 }       2、poll函数
       在单个线程中,select函数能够监视的文件描述符数量有最大的限制,一般为1024,可以修改内核将监视的文件描述符数量改大,但是这样会降低效率!这个时候就可以使用poll函数,poll函数本质上和select没有太大的差别,但是poll函数没有最大文件描述符限制,Linux应用程序中poll函数原型如下所示:
int poll(struct pollfd      *fds, nfds_t      nfds, int      timeout)      函数参数和返回值含义如下:
      fds:要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体pollfd类型的,pollfd结构体如下所示:
struct pollfd {
      int   fd;                /* 文件描述符 */
      short events;      /* 请求的事件 */
      short revents;      /* 返回的事件 */
};       fd是要监视的文件描述符,如果fd无效的话那么events监视事件也就无效,并且revents返回0。events是要监视的事件,可监视的事件类型如下所示:
POLLIN                        有数据可以读取。
POLLPRI                有紧急的数据需要读取。
POLLOUT                可以写数据。
POLLERR                指定的文件描述符发生错误。
POLLHUP                指定的文件描述符挂起。
POLLNVAL                无效的请求。
POLLRDNORM      等同于POLLIN       revents是返回参数,也就是返回的事件,有Linux内核设置具体的返回事件。
       nfds:poll函数要监视的文件描述符数量。
       timeout:超时时间,单位为ms。
       返回值:返回revents域中不为0的pollfd结构体个数,也就是发生事件或错误的文件描述符数量;0,超时;-1,发生错误,并且设置errno为错误类型。
       使用poll函数对某个设备驱动文件进行读非阻塞访问的操作示例如下所示:
示例代码 34.1.6 poll函数读非阻塞访问示例
1 void main(void)
2 {
3   int ret;
4   int fd;                              /* 要监视的文件描述符 */
5   struct pollfd fds;
6
7   fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
8
9   /* 构造结构体 */
10   fds.fd = fd;
11   fds.events = POLLIN;                /* 监视数据是否可以读取 */
12
13   ret = poll(&fds, 1, 500);                /* 轮询文件是否可操作,超时500ms */
14   if (ret) {                        /* 数据有效 */
15         ......
16         /* 读取数据 */
17         ......
18   } else if (ret == 0) {      /* 超时 */
19         ......
20   } else if (ret < 0) {      /* 错误 */
21         ......
22   }
23 }       3、epoll函数
       传统的selcet和poll函数都会随着所监听的fd数量的增加,出现效率低下的问题,而且poll函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。为此,epoll因运而生,epoll就是为处理大并发而准备的,一般常常在网络编程中使用epoll函数。应用程序需要先使用epoll_create函数创建一个epoll句柄,epoll_create函数原型如下:
int epoll_create(int size)       函数参数和返回值含义如下:
       size:从Linux2.6.8开始此参数已经没有意义了,随便填写一个大于0的值就可以。
       返回值:epoll句柄,如果为-1的话表示创建失败。
       epoll句柄创建成功以后使用epoll_ctl函数向其中添加要监视的文件描述符以及监视的事件,epoll_ctl函数原型如下所示:
int epoll_ctl(int      epfd, int      op, int      fd, struct epoll_event      *event)       函数参数和返回值含义如下:
       epfd:要操作的epoll句柄,也就是使用epoll_create函数创建的epoll句柄。
       op:表示要对epfd(epoll句柄)进行的操作,可以设置为:
EPOLL_CTL_ADD      向epfd添加文件参数fd表示的描述符。
EPOLL_CTL_MOD      修改参数fd的event事件。
EPOLL_CTL_DEL                从epfd中删除fd描述符。       fd:要监视的文件描述符。
       event:要监视的事件类型,为epoll_event结构体类型指针,epoll_event结构体类型如下所示:
struct epoll_event {
      uint32_t                events;      /* epoll事件 */
      epoll_data_t      data;                /* 用户数据 */
};      结构体epoll_event的events成员变量表示要监视的事件,可选的事件如下所示:
EPOLLIN                有数据可以读取。
EPOLLOUT                可以写数据。
EPOLLPRI                有紧急的数据需要读取。
EPOLLERR                指定的文件描述符发生错误。
EPOLLHUP                指定的文件描述符挂起。
EPOLLET                设置epoll为边沿触发,默认触发模式为水平触发。
EPOLLONESHOT      一次性的监视,当监视完成以后还需要再次监视某个fd,那么就需要将fd重新添加到epoll里面。       上面这些事件可以进行“或”操作,也就是说可以设置监视多个事件。
       返回值:0,成功;-1,失败,并且设置errno的值为相应的错误码。
       一切都设置好以后应用程序就可以通过epoll_wait函数来等待事件的发生,类似select函数。epoll_wait函数原型如下所示:
int epoll_wait(int                                 epfd,
                        struct epoll_event         *events,
                        int                                 maxevents,
                        int                                 timeout)       函数参数和返回值含义如下:
       epfd:要等待的epoll。
       events:指向epoll_event结构体的数组,当有事件发生的时候Linux内核会填写events,调用者可以根据events判断发生了哪些事件。
       maxevents:events数组大小,必须大于0。
       timeout:超时时间,单位为ms。
       返回值:0,超时;-1,错误;其他值,准备就绪的文件描述符数量。
       epoll更多的是用在大规模的并发服务器上,因为在这种场合下select和poll并不适合。当涉及到的文件描述符(fd)比较少的时候就适合用selcet和poll,本章我们就使用sellect和poll这两个函数。
1.1.4Linux驱动下的poll操作函数
       当应用程序调用select或poll函数来对驱动程序进行非阻塞访问的时候,驱动程序file_operations操作集中的poll函数就会执行。所以驱动程序的编写者需要提供对应的poll函数,poll函数原型如下所示:
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)       函数参数和返回值含义如下:
       filp:要打开的设备文件(文件描述符)。
       wait:结构体poll_table_struct类型指针,由应用程序传递进来的。一般将此参数传递给poll_wait函数。
       返回值:向应用程序返回设备或者资源状态,可以返回的资源状态如下:
POLLIN               有数据可以读取。
POLLPRI               有紧急的数据需要读取。
POLLOUT                可以写数据。
POLLERR                指定的文件描述符发生错误。
POLLHUP                指定的文件描述符挂起。
POLLNVAL                无效的请求。
POLLRDNORM      等同于POLLIN,普通数据可读       我们需要在驱动程序的poll函数中调用poll_wait函数,poll_wait函数不会引起阻塞,只是将应用程序添加到poll_table中,poll_wait函数原型如下:
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)       参数wait_address是要添加到poll_table中的等待队列头,参数p就是poll_table,就是file_operations中poll函数的wait参数。
       1.2阻塞IO实验
       在上一章Linux中断实验中,我们直接在应用程序中通过read函数不断的读取按键状态,当按键有效的时候就打印出按键值。这种方法有个缺点,那就是keyApp这个测试应用程序拥有很高的CPU占用率,大家可以在开发板中加载上一章的驱动程序模块keyirq.ko,然后以后台运行模式运行上一章的应用测试程序keyApp,命令如下:
./keyApp /dev/key &       测试驱动是否正常工作,如果驱动工作正常的话输入“top”命令查看keyApp这个应用程序的CPU使用率,结果如所示:

图 34.2.1 keyApp程序CPU使用率
       从图 34.2.1可以看出,keyApp这个应用程序的CPU使用率竟然高达45.4%,这仅仅是一个读取按键值的应用程序,这么高的CPU使用率显然是有问题的!原因就在于我们是直接在while循环中通过read函数读取按键值,因此keyApp这个软件会一直运行,一直读取按键值,CPU使用率肯定就会很高。最好的方法就是在没有有效的按键事件发生的时候,让keyApp这个应用程序应该处于休眠状态,当有按键事件发生以后keyApp这个应用程序才运行,打印出按键值,这样就会降低CPU使用率,本小节我们就使用阻塞IO来实现此功能。
1.2.1硬件原理图分析
       本章实验硬件原理图参考31.2小节即可。
1.2.2实验程序编写
       1、驱动程序编写
       本实验对应的例程路径为:领航者ZYNQ开发板光盘资料(A盘)\4_SourceCode\3_Embedded_Linux\Linux驱动例程\14_blockio
       本章实验我们在上一章的“13_irq”实验的基础上完成,主要是对其添加阻塞访问相关的代码。在drivers目录下新建名为“14_blockio”的文件夹,将“13_irq”实验中的keyirq.c复制到14_blockio文件夹中,并重命名为blockio.c。接下来我们就修改blockio.c这个文件,在其中添加阻塞相关的代码,完成以后的blockio.c内容如下所示(因为是在上一章实验的keyirq.c文件的基础上修改的,为了减少篇幅,下面的代码有省略):
示例代码 34.2.1 blockio.c文件代码(有省略)
......
40 /* 按键设备结构体 */
41 struct key_dev {
42   dev_t devid;                              /* 设备号 */
43   struct cdev cdev;                        /* cdev结构体 */
44   struct class *class;                        /* 类 */
45   struct device *device;                /* 设备 */
46   int key_gpio;                              /* GPIO编号 */
47   int irq_num;                              /* 中断号 */
48   struct timer_list timer;                /* 定时器 */
49   wait_queue_head_t r_wait;      /* 读等待队列头 */
50 };
51
52 static struct key_dev key;                /* 按键设备 */
53 static atomic_t status;                /* 按键状态 */
......
75 static ssize_t key_read(struct file *filp, char __user *buf,
76             size_t cnt, loff_t *offt)
77 {
78   int ret;
79
80   /* 加入等待队列,当有按键按下或松开动作发生时,才会被唤醒 */
81   ret = wait_event_interruptible(key.r_wait, KEY_KEEP != atomic_read(&status));
82   if (ret)
83         return ret;
84
85   /* 将按键状态信息发送给应用程序 */
86   ret = copy_to_user(buf, &status, sizeof(int));
87
88   /* 状态重置 */
89   atomic_set(&status, KEY_KEEP);
90
91   return ret;
92 }
......
118 static void key_timer_function(unsigned long arg)
119 {
120   static int last_val = 1;
121   int current_val;
122
123   /* 读取按键值并判断按键当前状态 */
124   current_val = gpio_get_value(key.key_gpio);
125   if (0 == current_val && last_val) {                        // 按下
126         atomic_set(&status, KEY_PRESS);
127         wake_up_interruptible(&key.r_wait);                // 唤醒r_wait队列头中的所有队列
128   }
129   else if (1 == current_val && !last_val) {                // 松开
130         atomic_set(&status, KEY_RELEASE);
131         wake_up_interruptible(&key.r_wait);                // 唤醒r_wait队列头中的所有队列
132   }
133   else
134         atomic_set(&status, KEY_KEEP);                        // 状态保持
135
136   last_val = current_val;
137 }
......
228 static int __init mykey_init(void)
229 {
      ......
232   /* 初始化等待队列头 */
233   init_waitqueue_head(&key.r_wait);
      ......
273   /* 初始化按键状态 */
274   atomic_set(&status, KEY_KEEP);
275
276   /* 初始化定时器 */
277   init_timer(&key.timer);
278   key.timer.function = key_timer_function;
279
280   return 0;
281
282 out4:
283   class_destroy(key.class);
284
285 out3:
286   cdev_del(&key.cdev);
287
288 out2:
289   unregister_chrdev_region(key.devid, KEY_CNT);
290
291 out1:
292   free_irq(key.irq_num, NULL);
293   gpio_free(key.key_gpio);
294
295   return ret;
}
......       第41~50行,我们删除了设备结构体struct key_dev中的自旋锁变量,本章驱动代码我们不用自旋锁,而改用原子变量来实现对相应变量的保护操作。第49行,添加了一个等待队列头r_wait,因为在Linux驱动中处理阻塞IO需要用到等待队列,因为等待队列会使进程进入休眠状态,所以不能使用自旋锁!
       第53行,定义了一个原子变量status,可以实现对该变量的原子操作,保护数据。
       第75~92行,key_read函数,在这个函数中我们会判断按键是否有按下或松开动作发生时,如果没有则调用wait_event_interruptible把它加入等待队列当中,进行阻塞,如果等待队列被唤醒并且条件“KEY_KEEP != atomic_read(&status)”成立则会解除阻塞,继续下面的操作,也就是读取按键状态数据将其发送给应用程序,因为采用了wait_event_interruptible函数,因此进入休眠态的进程可以被信号打断;在该函数中我们读取status原子变量必须要使用atomic_read函数进行操作。
       第118~137行,定时器定时处理函数key_timer_function,对按键状态进行判断,如果是按下动作或是松开动作则会使用wake_up_interruptible函数唤醒等待队列,并且将status变量设置为KEY_PRESS或KEY_RELEASE;这样在key_read函数中阻塞的进程就会解除阻塞继续进行下面的操作。
       第233行,在驱动入口函数中我们会调用init_waitqueue_head初始化等待队列头;第274行使用原子操作atomic_set设置按键的初始状态status为KEY_KEEP。
       使用等待队列实现阻塞访问重点注意两点:
       ①、将任务或者进程加入到等待队列头,
       ②、在合适的点唤醒等待队列,一般是中断处理函数里面。
       2、编写测试APP
       本节实验的测试APP直接使用13_irq实验目录下的keyApp.c测试程序,将keyApp.c复制到本实验目录14_blockio中,不需要修改任何内容。
1.2.3运行测试
       1、编译驱动程序和测试APP
       ①、编译驱动程序
       编写Makefile文件,将13_irq实验目录下的Makefile文件拷贝到本实验目录下,打开该Makefile文件,将obj-m变量的值改为blockio.o,Makefile内容如下所示:
示例代码 34.2.2 Makefile文件
1 KERN_DIR := /home/zynq/linux/kernel/linux-xlnx-xilinx-v2018.3
2
3 obj-m := blockio.o
4
5 all:
6         make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERN_DIR) M=`pwd` modules
7
8 clean:
9         make -C $(KERN_DIR) M=`pwd` clean       第3行,设置obj-m变量的值为blockio.o。
       文件修改完成之后保存退出,输入如下命令编译出驱动模块文件:
make       编译成功以后就会生成一个名为“blockio.ko”的驱动模块文件,如下所示:

图 34.2.2 编译驱动模块
      ①、编译测试APP
      输入如下命令编译测试keyApp.c这个测试程序:
arm-linux-gnueabihf-gcc keyApp.c -o keyApp       编译成功以后就会生成keyApp这个应用程序。
       2、运行测试
       将上一小节编译出来blockio.ko和keyApp这两个文件拷贝到开发板根文件系统/lib/modules/4.14.0-xilinx目录中,重启开发板,进入到目录/lib/modules/4.14.0-xilinx中,输入如下命令加载blockio.ko驱动模块:
depmod                              //第一次加载驱动的时候需要运行此命令
modprobe blockio.ko      //加载驱动       驱动加载成功以后使用如下命令打开keyApp这个测试APP,并且以后台模式运行:
./keyApp /dev/key &       按下开发板上的PS_KEY0按键,结果如图 34.2.3所示:

图 34.2.3 测试APP运行测试
      当按下或者松开PS_KEY0按键动作发生的时候,终端就会打印出相应的字符串信息。输入“top”命令,查看keyAPP这个应用APP的CPU使用率,如所示:

图 34.2.4 keyApp程序CPU占用率
       从图 34.2.4可以看出,当我们在按键驱动程序里面加入阻塞访问以后,keyApp这个应用程序的CPU使用率从图 34.2.1中的45.4%降低到了0.0%。大家注意,这里的0.0%并不是说keyApp这个应用程序不使用CPU了,只是因为使用率太小了,CPU使用率可能为0.00001%,但是图 34.2.4中只能显示出小数点后一位,因此就显示成了0.0%。
       我们可以使用“kill”命令关闭后台运行的应用程序,比如我们关闭掉keyApp这个后台运行的应用程序。首先输出“Ctrl+C”关闭top命令界面,进入到命令行模式。然后使用“ps”命令查看一下keyApp这个应用程序的PID,如所示:

图 34.2.5 查看进程PID
       从图 34.2.5可以看出,keyApp这个应用程序的PID为1240,使用“kill -9 PID”即可“杀死”指定PID的进程,比如我们现在要“杀死”PID为1240的keyApp应用程序,可是使用如下命令:
kill -9 1240       输入上述命令以后终端显示如图 34.2.6所示:

图 34.2.6 kill命令输出结果
       从图 34.2.6可以看出,“./keyApp /dev/key”这个应用程序已经被“杀掉”了,在此输入“ps”命令查看当前系统运行的进程,会发现keyApp已经不见了。这个就是使用kill命令“杀掉”指定进程的方法。
       1.3非阻塞IO实验
1.3.1硬件原理图分析
       本章实验硬件原理图参考31.2小节即可。
1.3.2实验程序编写
       1、驱动程序编写
       本实验对应的例程路径为:领航者ZYNQ开发板光盘资料(A盘)\4_SourceCode\3_Embedded_Linux\Linux驱动例程\15_noblockio
       本章我们将在上一节实验“14_blockio”实验的基础上完成,上一节实验我们已经在驱动中添加了阻塞IO的代码,本小节我们继续完善驱动,加入非阻塞IO驱动代码。在drivers目录下新建名为“15_noblockio”的文件夹,将“14_blockio”实验中的blockio.c复制到15_noblockio文件夹中,并重命名为noblockio.c。接下来我们就修改noblockio.c这个文件,在其中添加非阻塞相关的代码,完成以后的noblockio.c内容如下所示(因为是在上一小节实验的blockio.c文件的基础上修改的,为了减少篇幅,下面的代码有省略):
示例代码 34.3.1 noblockio.c文件(有省略)
1 /***************************************************************
2Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
3文件名    : noblockio.c
4作者      : 邓涛
5版本      : V1.0
6描述      : 非阻塞IO访问驱动实验
7其他      : 无
8论坛      : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
9日志      : 初版V1.0 2019/1/30 邓涛创建
10***************************************************************/
11
12 #include <linux/types.h>
13 #include <linux/kernel.h>
14 #include <linux/delay.h>
15 #include <linux/ide.h>
16 #include <linux/init.h>
17 #include <linux/module.h>
18 #include <linux/errno.h>
19 #include <linux/gpio.h>
20 #include <asm/mach/map.h>
21 #include <asm/uaccess.h>
22 #include <asm/io.h>
23 #include <linux/cdev.h>
24 #include <linux/of.h>
25 #include <linux/of_address.h>
26 #include <linux/of_gpio.h>
27 #include <linux/of_irq.h>
28 #include <linux/irq.h>
29 #include <linux/poll.h>
......
68 /*
69* @description                : 从设备读取数据
70* @param – filp                : 要打开的设备文件(文件描述符)
71* @param – buf                : 返回给用户空间的数据缓冲区
72* @param – cnt                : 要读取的数据长度
73* @param – offt                : 相对于文件首地址的偏移
74* @return                        : 读取的字节数,如果为负值,表示读取失败
75*/
76 static ssize_t key_read(struct file *filp, char __user *buf,
77             size_t cnt, loff_t *offt)
78 {
79   int ret;
80
81   if (filp->f_flags & O_NONBLOCK) {      // 非阻塞方式访问
82         if(KEY_KEEP == atomic_read(&status))
83             return -EAGAIN;
84   } else {                                                      // 阻塞方式访问
85         /* 加入等待队列,当有按键按下或松开动作发生时,才会被唤醒 */
86         ret = wait_event_interruptible(key.r_wait, KEY_KEEP != atomic_read(&status));
87         if (ret)
88             return ret;
89   }
90
91   /* 将按键状态信息发送给应用程序 */
92   ret = copy_to_user(buf, &status, sizeof(int));
93
94   /* 状态重置 */
95   atomic_set(&status, KEY_KEEP);
96
97   return ret;
98 }
......
100 /*
101* @description                : poll函数,用于处理非阻塞访问
102* @param – filp                : 要打开的设备文件(文件描述符)
103* @param – wait                : 等待列表(poll_table)
104* @return                        : 设备或者资源状态,
105*/
106 static unsigned int key_poll(struct file *filp, struct poll_table_struct *wait)
107 {
108   unsigned int mask = 0;
109
110   poll_wait(filp, &key.r_wait, wait);
111
112   if(KEY_KEEP != atomic_read(&status))      // 按键按下或松开动作发生
113         mask = POLLIN | POLLRDNORM;      // 返回PLLIN
114
115   return mask;
116 }
......
245 /* 设备操作函数 */
246 static struct file_operations key_fops = {
247   .owner                = THIS_MODULE,
248   .open                = key_open,
249   .read                        = key_read,
250   .write                = key_write,
251   .release                = key_release,
252   .poll                        = key_poll,
253 };
......
325 static void __exit mykey_exit(void)
326 {
327   /* 删除定时器 */
328   del_timer_sync(&key.timer);
329
330   /* 注销设备 */
331   device_destroy(key.class, key.devid);
332
333   /* 注销类 */
334   class_destroy(key.class);
335
336   /* 删除cdev */
337   cdev_del(&key.cdev);
338
339   /* 注销设备号 */
340   unregister_chrdev_region(key.devid, KEY_CNT);
341
342   /* 释放中断 */
343   free_irq(key.irq_num, NULL);
344
345   /* 释放GPIO */
346   gpio_free(key.key_gpio);
347 }
348
349 /* 驱动模块入口和出口函数注册 */
350 module_init(mykey_init);
351 module_exit(mykey_exit);
352
353 MODULE_AUTHOR("DengTao <<a href="mailto:773904075@qq.com">773904075@qq.com</a>>");
354 MODULE_DESCRIPTION("Gpio Key Interrupt Driver");
355 MODULE_LICENSE("GPL");       第29行,使用include将内核源码目录include/linux/poll.h头文件包含进来。
       第76~98行,key_read函数中判断是否为非阻塞式读取访问,如果是的话就判断按键状态是否有效,也就是判断是否产生了按下或松开这样的动作,如果没有的话就返回-EAGAIN。
       第106~116行,key_poll函数就是file_operations驱动操作集中的poll函数,当应用程序调用select或者poll函数的时候key_poll函数就会执行。第110行调用poll_wait函数将等待队列头添加到poll_table中,第112~113行判断按键是否有效,如果按键有效的话就向应用程序返回POLLIN这个事件,表示有数据可以读取。
       第252行,设置file_operations的poll成员变量为key_poll。
       2、编写测试APP
       将上一节实验目录14_blockio下的keyApp.c源文件拷贝到本实验目录下,然后打开keyApp.c文件进行修改,修改完成之后内容如下所示:
示例代码 34.3.2 keyApp.c文件代码
1 /***************************************************************
2Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
3文件名         : keyApp.c
4作者             : 邓涛
5版本             : V1.0
6描述             : 以非阻塞方式读取按键状态
7其他             : 无
8使用方法         : ./keyApp /dev/key
9论坛             : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
10日志             : 初版V1.0 2019/1/30 邓涛创建
11***************************************************************/
12
13 #include <stdio.h>
14 #include <unistd.h>
15 #include <sys/types.h>
16 #include <sys/stat.h>
17 #include <fcntl.h>
18 #include <stdlib.h>
19 #include <string.h>
20 #include <poll.h>
21
22 /*
23* @description                : main主程序
24* @param – argc                : argv数组元素个数
25* @param – argv                : 具体参数
26* @return                        : 0 成功;其他 失败
27*/
28 int main(int argc, char *argv[])
29 {
30   fd_set readfds;
31   int key_val;
32   int fd;
33   int ret;
34
35   /* 判断传参个数是否正确 */
36   if(2 != argc) {
37         printf("Usage:\n"
38                "\t./keyApp /dev/key\n"
39               );
40         return -1;
41   }
42
43   /* 打开设备 */
44   fd = open(argv, O_RDONLY | O_NONBLOCK);
45   if(0 > fd) {
46         printf("ERROR: %s file open failed!\n", argv);
47         return -1;
48   }
49
50   FD_ZERO(&readfds);
51   FD_SET(fd, &readfds);
52
53   /* 循环轮训读取按键数据 */
54   for ( ; ; ) {
55
56         ret = select(fd + 1, &readfds, NULL, NULL, NULL);
57         switch (ret) {
58
59         case 0:                // 超时
60             /* 用户自定义超时处理 */
61             break;
62
63         case -1:                // 错误
64             /* 用户自定义错误处理 */
65             break;
66
67         default:
68             if(FD_ISSET(fd, &readfds)) {
69               read(fd, &key_val, sizeof(int));
70               if (0 == key_val)
71                     printf("Key Press\n");
72               else if (1 == key_val)
73                     printf("Key Release\n");
74             }
75
76             break;
77         }
78   }
79
80   /* 关闭设备 */
81   close(fd);
82   return 0;
83 }       第54~78行,在本测试程序中我们使用select函数来实现非阻塞访问,在for循环中使用select函数不断的轮询,检查驱动程序是否有数据可以读取,如果可以读取的话就调用read函数读取按键数据。大家也可以试试使用poll函数来实现!
1.3.3运行测试
       1、编译驱动程序和测试APP
       ①、编译驱动程序
       编写Makefile文件,将上一节实验目录14_blockio下的Makefile文件拷贝到本实验目录下,打开Makefile文件,将obj-m变量的值改为noblockio.o,Makefile内容如下所示:
示例代码 34.3.3 Makefile文件
1 KERN_DIR := /home/zynq/linux/kernel/linux-xlnx-xilinx-v2018.3
2
3 obj-m := noblockio.o
4
5 all:
6         make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERN_DIR) M=`pwd` modules
7
8 clean:
9         make -C $(KERN_DIR) M=`pwd` clean       第3行,设置obj-m变量的值为noblockio.o。
       文件修改完成之后保存退出,输入如下命令编译出驱动模块文件:
make       编译成功以后就会生成一个名为“noblockio.ko”的驱动模块文件。
       ①、编译测试APP
       输入如下命令编译测试keyApp.c这个测试程序:
arm-linux-gnueabihf-gcc keyApp.c -o keyApp       编译成功以后就会生成keyApp这个应用程序。
       2、运行测试
       将上一小节编译出来noblockio.ko和keyApp这两个文件拷贝到开发板根文件系统/lib/modules/4.14.0-xilinx目录中,重启开发板,进入到目录/lib/modules/4.14.0-xilinx 5中,输入如下命令加载blockio.ko驱动模块:
depmod                              //第一次加载驱动的时候需要运行此命令
modprobe noblockio.ko      //加载驱动       驱动加载成功以后使用如下命令打开keyApp这个测试APP,并且以后台模式运行:
./keyApp /dev/key &       按下开发板上的PS_KEY0按键,结果如图 34.3.1所示:

图 34.3.1 测试APP运行测试
       当产生按下按键或松开按键动作的时候,终端就会打印出相应的字符串信息。输入“top”命令,查看keyAPP这个应用APP的CPU使用率,如所示:

图 34.3.2 应用程序CPU使用率
       从图 34.3.2可以看出,采用非阻塞方式读处理以后,keyApp的CPU占用率也低至0.0%,和图 34.2.4中的一样,这里的0.0%并不是说keyApp这个应用程序不使用CPU了,只是因为使用率太小了,而图中只能显示出小数点后一位,因此就显示成了0.0%。
       如果要“杀掉”处于后台运行模式的keyApp这个应用程序,可以参考34.2.3小节讲解的方法。
页: [1]
查看完整版本: 《领航者ZYNQ之嵌入式Linux开发指南_V2.0》第三十四章 Linux IO实验