Linux系统编程 / triggerhappy 源码分析(3.select 的应用)

哈喽,我是老吴,继续记录我的学习心得。

一、进步的滞后性

我们期望进步是线性:
每一个人付出一些努力后,都希望它有立竿见影的效果。

现实是:
做出努力后,结果的显现往往滞后。

只有在几个月或几年后,我们才意识到以前学习/工作的真正价值。

“失望谷地” 的出现:
人们在投入数周或数月的辛勤工作后,却没有任何看得见的效果,于是进入深感沮丧的时期。

如何改变:
1> 改变意识。功夫并没有白费,它只是蓄积起来了。直到很久以后,以前努力的全部价值才会显露出来。

2> 寻找一些可以帮助自己对抗滞后进步所带来的低落情绪的技巧。例如培养习惯的四大定律,具体的如即时奖励、习惯追踪等。


二、triggerhappy 源码分析 / 3.select() 的应用

正文目录:

1. thd.c / process_events() 源码分析
2. I/O 多路复用 ( Multiplexing ) 是为了解决什么问题
3. I/O 多路复用设计思路
4. select() 的用法
5. 和 poll/epoll 对比(补充知识,非重点)
6. triggerhappy:thd.c / process_devices() 源码分析
7. 相关参考

写作目的:

  • 通过阅读 triggerhappy 的源码,学习 I/O 多路复用的方法之一 select() 的使用方法。

测试环境:

  • Ubuntu 16.04

  • Gcc 5.4.0

1. thd.c / process_events() 源码分析

1.1 process_events() 的作用

1) 作用:

  • 监控 input 设备;

  • 读取 input 事件,并执行相应的 action;

  • 检查 socket 是否有接受到命令。

2) 调用流程:

thd.cmain()start_readers()process_events()

1.2 process_events() 的内容:4 个步骤

1) 建立主循环:

while ( count_devices() > 0 || cmd_fd != -1 ) {[...]
}

2) 在主循环内,使用 select 监测多个 input 设备文件 (重点):

while(...) {[...]       // some init for selectretval = select(max_fd+1, &rfds, NULL, NULL, &tv);
}

本文的重点就是了解 select 相关的知识点。

3) select 返回后,调用 process_devices() 读取数据 (重点):

while(...) {[...]       // some init for selectretval = select(max_fd+1, &rfds, NULL, NULL, &tv);if (retval) {process_devices();}
}

process_devices() 是 triggerhappy 的重点函数。

先大致了解一下它的作用:
对于每一个有待读数据的 input 设备文件,都调用 read() 函数读取数据,然后解析数据,根据解析结果去执行相应的 action,action 在 example.conf 中定义:

$ cat /etc/triggerhappy/triggers.d/example.conf
KEY_VOLUMEUP    1        /usr/bin/amixer set Master 5%+
...

/usr/bin/amixer set Master 5%+ 就是一个 action。

暂时不用太深入,后续会专门写一篇文章来详细分析 process_devices()。

4) 是否有接收到用户命令 (th-cmd)?

while(...) {[...]if (retval) {process_devices();if ( cmd_fd != -1 && FD_ISSET( cmd_fd, &rfds ) ) {struct command *cmd = read_command( cmd_fd );obey_command( cmd );free(cmd);}}
}

cmd_fd 是一个 socket:

main()start_readers()cmd_fd = bind_cmdsocket(cmd_file);cmd_fd = socket(AF_UNIX, SOCK_DGRAM, 0);

triggerhappy 支持使用 socket 通信以动态增加和删除输入设备,例如动态地添加要监测的输入设备 /的 /dev/input/event0:

$ thd --socket /var/run/triggerhappy.socket
$ th-cmd --socket /var/run/triggerhappy.socket --add /dev/input/event0

read_command() 和 obey_command() 负责读取和解析用户通过 socket 发送过来的命令,后面找时间写一些关于 domain socket 的文章。

接下来,我们先学习一下 select 的机制、使用方法和注意事项

注意:本文的写作目的不是为了描述 select() 完整的使用方法和内部实现,而是在有限的篇幅内尽量整理一下 I/O 多路复用相关的知识点,即仅仅是引子,更完整的内容仍需要小伙伴自行阅读相关书籍。

2. I/O 多路复用 ( Multiplexing ) 是为了解决什么问题: 多文件阻塞

应用通常需要在多个文件描述符上阻塞,例如在键盘输入、进程间通信以及其他文件之间协调 I/O。

很多情况下,一个文件描述符依赖另一个文件描述符。只要是有 1 个文件描述符数据还没有准备好,比如发送了 read() 调用,但是还没有任何数据,就进程会阻塞在此 I/O 操作上,此时无法对其他的文件描述符提供服务。这导致应用效率变低,影响用户体验。

尤其是对于网络应用而言,可能会同时打开多个 socket,如果应用阻塞在某个 socket上,将引发很多问题。

设计 I/O 多路复用就是为了解决上述问题:
支持应用同时在多个文件描述符 (普通文件、管道、套接字...) 上阻塞,并在其中某个文件描述符可以读写时收到通知。

3. I/O 多路复用设计思路

1> 无 I/O 就绪:在有可用的文件描述符之前一直处于睡眠状态;

2> 任意文件描述符 I/O 就绪:唤醒应用,应用检查是哪个文件描述符可用了;

3> 执行 I/O 操作:应用处理所有 I/O 就绪的文件描述符,此时没有阻塞;

4> 返回第 1> 步:重新开始;

Linux 提供了三种 I/O 多路复用方案:
-select()、poll() 和 epoll()。

本文先将学习重点放在 select() 上,同时也稍微了解一下 poll() 和 epoll(),明确三者的优缺点。

4. select() 的用法

4.1 函数概述

$ man 2 select/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • select() 会监测 3 个 文件描述符集,直到有 1 个或多个文件描述符集成为就绪态

  • 在给定的文件描述符 I/O 就绪之前并且还没有超出指定的时间限制,select() 会阻塞。

4.2 参数说明

1) 参数 fd_set * readfds、writefds、exceptfds:

  • select() 监测 3 种 I/O 事件(读、写、异常),每 1 种事件对应 1 个文件描述符集。

  • 每个描述符集存储在一个 fd_set 数据类型中,可以认为它是一个很大的位数组 (array of bits):


  • select() 中间 3 个文件描述符集参数 readfds、writefds 和 exceptfds 分别对应了我们关心的可读、可写或处于异常条件的文件描述符集。

2) 操作文件描述符集的几个宏:

  • FD_ZERO:从指定集合中删除所有的文件描述符;

  • FD_SET:向指定集中添加一个文件描述符,而FD_CLR则从指定集中删除一个文件描述符;

  • FD_ISSET:检查一个文件描述符是否在给定集合中;

  • 由于文件描述符集是静态建立的,所以文件描述符数存在上限值,而且存在最大文件描述符值,这两个值都是由 FD_SETSIZE 设置。在 Linux 中,该值是1024;

3) 参数 int nfds:最大文件描述符 + 1

  • 参数 nfds 必须设为比 3 个文件描述符集合中所包含的最大文件描述符 + 1 的值。

  • 传递该参数是为了让内核不用去检查大于这个值的文件描述符是否属于上述 3 个文件描述符集合,这也从侧面说明了 select() 在内核里实现是会遍历 0~nfds 文件描述符。

  • select() 的设计天生就没考虑到用于监测大量文件 I/O 就绪的情景。但是在 triggerhappy 里使用 select() 则是合理的,这是因为 一个系统上的 input 设备并不会太多,本文末尾还会对 select() 的优缺点进行总结。

4) 参数 struct timeval *timeout:

  • timeout == NULL:永远等待。当所指定的描述符中的之一已就绪或捕捉到一个信号则返回。如果捕捉到一个信号,则返回 -1,errno 设置为 EINTR。

  • timeout->tv_sec == 0 && timeout->tv_usec == 0:完全不等待。测试所有指定的描述符并立即返回。

  • timeout->tv_sec != 0 || timeout->tv_usec != 0:等待指定的秒数和微秒数。当指定的描述符之一已就绪,或超过指定的时间值时立即返回。如果在超时到期时还没有如何描述符准备好,则返回 0。

4.3 返回说明

1) return = -1:表示有错误发生。典型的错误码 (errno) 包括 EBADF 和 EINTR 等。

  • EBADF: 表示 readfds、writefds、exceptfds 中有非法文件描述符。

  • EINTR: 表示该调用被某个信号中断了。

2) return = 0:表示在有文件描述符成为就绪态之前 select() 已经超时。

  • 在这种情况下,每个返回的文件描述符集合将被清空。

3) return > 0:表示有 1 个或多个文件描述符已就绪。

  • 返回值表示处于就绪态的文件描述符个数。

  • 每个集合都修改成只包含相应类型的 I/O 就绪的文件描述符。

  • 每个集合都需要检查通过 FD_ISSET() 以此找出发生的 I/O 事件是什么。

  • 如果同一个文件描述符在 readfds、writefds 和 exceptfds 中同时被指定,且它对于多个 I/O 事件都处于就绪态的话,那么就会被统计多次。。例如同一描述符已准备好读和写,那么在返回值中会对其计 2 次

4.4 注意事项

  • 每次调用 select() 之前,都要对所有参数进行初始化;

  • 在 Linux 上,如果 select() 被信号中断的话,struct timeval 会被修改以表示剩余的超时时间;

  • exceptfds 常常被误解为在文件描述符上出现了异常情况,这是错误的。它其实与伪终端和流式套接字相关;

  • 如果在一个描述符上碰到了文件尾端,select() 会认为该描述符是可读的。此时调用 read(),会返回 0,这是 UNIX 系统指示到达文件尾端的方法。很多人错误地认为,当到达文件尾端时, select() 会指示一个异常条件。

  • 如果想更详细地了解 I/O 多路复用的使用方法,可以学习一下开源软件 libevent。

4.5 最简单的 demo 程序

1) 初始化select() 的参数:

#define TIMEOUT 5
#define BUF_LEN 1024int main (void)
{struct timeval tv;fd_set readfds;int ret;/* Wait on stdin for input. */FD_ZERO(&readfds);FD_SET(STDIN_FILENO, &readfds);/* Wait up to five seconds. */tv.tv_sec = TIMEOUT;tv.tv_usec = 0;[...]
}

2) select () 监测是否有 I/O 就绪:

int main (void)
{[...]   // 1) init select param/* All right, now block! */ret = select (STDIN_FILENO + 1, &readfds, NULL, NULL, &tv);if (ret == -1) {perror ("select");return 1;} else if (!ret) {printf ("%d seconds elapsed.\n", TIMEOUT);return 0;}[...]
}

3) 进行 I/O 操作:

int main (void)
{[...]   // 1) init select param[...]   // 2) do select()// 3) do I/Oif (FD_ISSET(STDIN_FILENO, &readfds)) {char buf[BUF_LEN+1];int len;len = read (STDIN_FILENO, buf, BUF_LEN);if (len == -1) {perror ("read");return 1;}if (len) {buf[len] = '\0';printf ("read: %s\n", buf);}}return 0;
}

4) 运行效果:

$ gcc simple_select.c -o simple_select
$ ./simple_select 
a
read: a

启动程序时,程序会阻塞,这时从键盘敲入 'a',程序会被唤醒。

5. 和 poll / epoll 进行简单对比(补充知识,非重点)

5.1 对比 poll()

$ man 2 poll#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);struct pollfd {int   fd;         /* file descriptor */short events;     /* requested events */short revents;    /* returned events */
};
  • poll()系统调用是 System V 的 I/O 多路复用解决方案。它解决了一些select()的不足,不过出于习惯或可移植性的考虑,select() 还是被频繁使用

  • poll 的监测对象是 struct pollfd 数组,它可以精准的指定要监测的文件集合。而 select() 的文件描述符集合是静态的,当文件集合是稀疏的时,例如要监测文件描述符 0 和 1000 时,select() 的效率比 poll() 低很多

  • select() 返回时会重新创建文件描述符集,因此每次调用都必须重新初始化。poll() 系统调用会把输入 (events 成员) 和输出 (revents 成员) 分离开,无需重新初始化数组就可以重新使用

5.2 对比 epoll

  • epoll 是 Linux 特有的 I/O 多路复用解决方案

  • epoll 的实现比 poll() 和 select() 要复杂得多,epoll 解决了前两个都存在的基本性能问题,并增加了一些新的特性。

  • poll() 和 select() 每次调用时,内核必须遍历所有被监视的文件描述符。当文件描述符列表变得很大时,例如包含几百个甚至几千个文件描述符时,每次调用都要遍历列表就变成规模上的瓶颈。而 epoll 监测的对象是事件,无需遍历文件列表,所以在性能上有很大的提升

  • select、poll、epoll 的后端都是 struct file_operations 里的 unsigned int (*poll) (struct file *, struct poll_table_struct *);。以后有机会的话,会继续跟进它们三者在内核里的实现。

6. 继续分析 triggerhappy:thd.c / process_devices():读取 input 数据

static void process_devices(void) {for_each_device( &check_device );
}

devices.c / for_each_device() 会遍历所有 input 设备(内含 input 文件描述符),对每一个设备都调用 check_device() 函数:

static void check_device( device *d ) {int fd = d->fd;if (FD_ISSET( fd, &rfds )) {if (read_event( d )) {/* read error? Remove the device! */remove_device( d->devname );}}
}

thd.c / read_event() 执行input 数据的 read 操作:

static int read_event( device *dev ) {[...]           // some initstruct input_event ev;int n = read( fd, &ev, sizeof(ev) );[...]           // parse and do actionrun_triggers( ev.type, ev.code, ev.value, *keystate, dev );
}

1 个input_event 事件相当于产生了一个 trigger,接下来的重点就是调用 trigger.c / run_triggers() 来触发用户自定义的 action 了。

鉴于大多数人的注意力无法在一篇文章里上集中太久,更多的内容将放在后面的文章里。建议大家可以先自行阅读相关书籍,不是自己理解到的东西是消化不了的。

7. 相关参考

  • 《UNIX 环境高级编程》,14,16,17.6 章节

  • 《Linux 系统编程》,2.10,4.2,8.7,11.7 章节

  • 《Linux/UNIX系统编程手册》,63 章节

  • Libevent


 推荐阅读:

    专辑|Linux文章汇总

    专辑|程序人生

    专辑|C语言

嵌入式Linux

微信扫描二维码,关注我的公众号

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/467315.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

今天,给我妈打电话聊了我爸

晚上&#xff0c;给我妈打电话&#xff0c;我爸前两天病了&#xff0c;前几年的大病虽然慢慢康复&#xff0c;但是因为年纪大了&#xff0c;身体机能也慢慢变弱&#xff0c;总是有一些大大小小的问题。前两天我发消息给我爸&#xff0c;我说我们要进新房子了&#xff0c;虽然房…

A Simple Note on P4FPGA: A Rapid Prototyping Framework for P4

论文&#xff1a;P4FPGA: A Rapid Prototyping Framework for P4 Github&#xff1a;https://github.com/p4fpga Reference: Han Wang, Robert Soule ́, Huynh Tu Dang, Ki Suh Lee, Vishal Shrivastav, Nate Foster, and Hakim Weatherspoon. 2017. P4FPGA : A Rapid Prototy…

客户端回调

Client Callback 是ASP.NET 2.0新增的一个特性。简单的说&#xff0c;就是在不刷新页面的情况下&#xff0c;用javascript向服务器端传递参数、调用服务器端的方法、并且得到服务器端的返回值进行处理。 1> Why Client Callback HTTP是无状态的协议。在HTTP协议之上开发的…

万事开头难 - 介绍IMX6ULL启动方式

不同开发板&#xff0c;启动方式不一样&#xff0c;今天我们来介绍imx6ull开发板的启动方式&#xff0c;这非常重要。若不了解清楚启动方式&#xff0c;后面的所有开发工作便无从谈起。本文摘自100ask_imx6ull 开发板 配套学习手册-《嵌入式Linux应用开发完全手册_韦东山全系列…

可以在中断服务程序执行malloc吗?

这是微信群里面的一个群友提的问题&#xff0c;原问题如下&#xff1a;今天遇到一个面试问题 请教各位老哥 中断服务程序能否malloc &#xff1f;为什么&#xff1f;我回答是 不行 因为中断服务程序应该要尽量简短&#xff0c;且执行完中断服务程序会跳出中断函数&#xff0c…

Android Input 子系统初探

Android系统基于Linux内核实现&#xff0c;内核作为整个操作系统的核心&#xff0c;对下&#xff0c;它负责整个硬件的驱动、实现对硬件器件的控制管理&#xff1b;对上&#xff0c;它提供各种系统所需的核心功能。Android系统支持的输入设备较多&#xff0c;如按键、触摸屏、手…

计算机excl知识题,2019职称计算机考试Excel模拟题及答案(1.18)

【导语】2019年职称计算机考试备考正在进行中&#xff0c;为了方便考生及时有效的备考&#xff0c;那么&#xff0c;无忧考网为您精心整理了2019职称计算机考试Excel模拟题及答案(1.18)&#xff0c;把握机会抓紧练习吧。如想获取更多职称计算机考试的模拟题及备考资料&#xff…

CPU中的程序是怎么运行起来的

总述最近一位朋友问我&#xff0c;开发的代码是怎么在芯片运行起来的&#xff0c;我就开始给他介绍代码的预编译、汇编、编译、链接然后到一般的文件属性&#xff0c;再到代码运行。但是大佬问了我一句&#xff0c;CPU到底是怎么执行到每一个逻辑的&#xff0c;就讲了哈CPU的架…

Git-远程操作

远程分支&#xff1a;远程跟踪分支remote branch是对远程分支状态的引用&#xff0c;是不能移动的&#xff0c;它会根据远程分支变化以及网络通信自动移动。Git服务器包含了远程分支master&#xff0c;在My Computer中的remote branch就是远程跟踪分支&#xff0c;是对git服务器…

对于鸿蒙的一点见解

周五我在东莞出差&#xff0c;下班后看了开发者大会的视频回放&#xff0c;回到酒店&#xff0c;我跟邓总说&#xff0c;我们今天不要拍视频&#xff0c;也不要打王者了&#xff0c;你给我下鸿蒙的代码来看看&#xff0c;然后&#xff0c;那天晚上我们都没有上线王者&#xff0…

Linux内存管理slub分配器

背景Kernel版本&#xff1a;4.14ARM64处理器&#xff0c;Contex-A53&#xff0c;双核使用工具&#xff1a;Source Insight 3.5&#xff0c; Visio1. 概述之前的文章分析的都是基于页面的内存分配&#xff0c;而小块内存的分配和管理是通过块分配器来实现的。目前内核中&#xf…

逆向知识第十讲,循环在汇编中的表现形式,以及代码还原

逆向知识第十讲,循环在汇编中的表现形式,以及代码还原 一丶do While在汇编中的表现形式 1.1高级代码: #include "stdafx.h"int main(int argc, char* argv[]) {int nSum 0;int i 0;do {nSum nSum i;} while (i <100);return 0; } 高级代码很简单,只是一个简单…

做技术知道了哪些事情代表自己成熟了?

如果技术圈是一个江湖&#xff0c;每个人初入江湖的时候都懵懵懂懂的&#xff0c;从懵懂到老练&#xff0c;从老练到老油条&#xff0c;这个是一个过程&#xff0c;过程中总是有一些比较有用的观点&#xff0c;这些观点&#xff0c;就表示你从小白上升到老白的过渡。这些观点&a…

数据结构复习笔记(2)

1&#xff0c; 若入栈的元素为n,则可得到的输出序列数量为 (2n)!/(n1)(n!)(n!)。2&#xff0c; 用两个长度相同的栈S1,S2构造一个队列。在S1中进行入队操作&#xff0c;S2中进行出队操作 &#xff0c;判断队列空的条件是&#xff0c;S1和S2同时为空&#xff0c;判断队列满的条…

用于MCU,基于FreeRTOS的micro(轻量级)ROS

编辑整理&#xff1a;strongerHuang作者&#xff1a;Francesca Finocchiaro关注我的读者中应该有部分是做ROS相关的工作&#xff0c;今天就来分享一个基于FreeRTOS的micro&#xff08;微型&#xff09;ROS。一、关于ROSROS&#xff1a;Robot Operating System,&#xff0c;即机…

【干货】同步与互斥的失败例子

韦东山老师最新录制的驱动大全之<<同步与互斥>>收费视频已经在淘宝上架销售 &#xff0c;一共7节&#xff0c;良心价29元&#xff0c;同时已经同步到CSDN , 51CTO , 电子发烧友&#xff0c;腾讯课堂等平台。本文是其中一节《同步与互斥的失败例子》视频配套文档&am…

TCP三次握手

以下是我做的实验 &#xff0c;180.97.33.108 是百度 以下是我自己画的图 转载于:https://www.cnblogs.com/heben/p/7879439.html

Linux中断子系统-通用框架处理

背景Kernel版本&#xff1a;4.14ARM64处理器&#xff0c;Contex-A53&#xff0c;双核使用工具&#xff1a;Source Insight 3.5&#xff0c; Visio1. 概述《Linux中断子系统&#xff08;一&#xff09;-中断控制器及驱动分析》讲到了底层硬件GIC驱动&#xff0c;以及Arch-Specif…

接口测试工具-fiddler的运用

本篇主要介绍一下fiddler的基本运用&#xff0c;包括查看接口请求方式&#xff0c;状态响应码&#xff0c;如何进行接口测试等 一&#xff0e;Fiddler的优点 独立的可以直接抓http请求小巧、功能完善快捷、启动就行代理方便二&#xff0e;什么是Fiddler Fiddler是一个http协议调…

微电子科学与工程要学计算机吗,微电子科学与工程专业就业前景如何 有前途吗...

微电子科学与工程专业就业前景如何&#xff1f;有前途吗&#xff1f;下面小编为大家整理了相关内容&#xff0c;以供参考&#xff0c;一起来看看吧&#xff01;微电子科学与工程专业就业前景微电子科学与工程专业近年来也逐渐热火起来了&#xff0c;竞争力也很大。微电子专业一…