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,一经查实,立即删除!

相关文章

node中定时器, process.nextTick(), setImediate()的区别与联系

1.定时器 setTimeout()和setInterval()与浏览器中的API是一致的&#xff0c;定时器的问题在于&#xff0c;他并非精确的&#xff08;在容忍范围内&#xff09;。尽管事件循环十分快&#xff0c;但是如果某一次循环占用的时间较多&#xff0c;那么下次循环时&#xff0c;他也许已…

[转载]关于NETBIOS理清楚概念的文章

有关网上邻居的问题&#xff0c;问的人一直比较多&#xff0c;在理解上存在的误区也普遍较为严重。鉴于Microsoft的NETBIOS文档不是很细致&#xff0c;我四处收集了一些相关资料加上自己的实践经验写了这个系列&#xff0c;希望能对大家有所帮助&#xff0e; 本来想为了增加可读…

form.html,HTML表单form

前面我们讲了HTML的各种数据输入方式&#xff0c;数据输入之后&#xff0c;一般来说应该提交给后台处理&#xff0c;HTML通过表单form来提交数据。form包含1个或多个数据输入&#xff0c;如input&#xff0c;单选&#xff0c;复选&#xff0c;文件等等form将其包含的输入项&…

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

晚上&#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协议之上开发的…

初步使用计算机说课,初步认识计算机说课稿

探索、自主发现式学习。根据教材特点与学生实际&#xff0c;我制订以下三维学习目标&#xff1a;1、了解计算机的历史和发展趋势。2、认识计算机的系统组成。3、激发学生学习计算机硬件知识的兴趣。4、提高学生学习、使用计算机的兴趣和互相合作意识品质。三、教学环境&#xf…

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

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

软件架构培训

在沈阳参加了软件架构培训&#xff0c;感觉非常好&#xff0c;老师讲的很适合我们的应用。真是受益非浅啊&#xff0c;把具体培训内容贴上&#xff1a; 课程名称&#xff1a;OOSE培训 课程目标&#xff1a; • 从广义角度理解软件框架的形成过程&#xff08;RUP的过程裁剪&…

SVN cleanup 反复失败解决办法

svn cleanup cleaning up 操作反复失败&#xff0c;svn提示的问题是版本需要更新&#xff0c;更新成最新的版本之后&#xff0c;依旧反复失败&#xff0c;陷入死循环。还好找一个blog上的方法试了一下&#xff0c;成功了。先说故障环境&#xff1a;OS: win7 x64TortoiseSVN 1.9…

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

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

my eclipse 类似dreamweaver编辑html,8款替代Dreamweaver的开源网页开发工具

Adobe Dreamweaver虽然非常好用&#xff0c;但它并不是唯一一个能够设计、开发、发布精彩网站的Web开发集成环境。我们的开源世界里有很多非常棒的可以完全替代Dreamweaver的各种功能的优秀Web开发工具&#xff0c;更重要的&#xff0c;是免费的。如果你正在寻找Dreamweaver的替…

Android Input 子系统初探

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

Ruby零碎笔记

Ruby零碎笔记 飞机上阅读pdf的笔记&#xff0c;因为不联网&#xff0c;内容不多而且比较零散&#xff0c;以tips的形式记录 tips 查看当前作用域的变量puts local_variables ruby中方法传递参数时&#xff0c;括号是可选的脚本开始运行时&#xff0c;main对象会被自动创建&…

网管日志-06.07.25

昨天晚上回家的时候已经是23:30左右了&#xff0c;尽管电信的工程师已经测试出了光信号衰减状况&#xff0c;也进行了相应的处理&#xff0c;最后达到了单芯光纤-11db的“理想”状态&#xff0c;可能用户也对这个值非常满意&#xff0c;我们也折腾到了大半夜。我是在地下网络机…

html5控制符置于底层,HTML5占位符在焦点上消失

Stefano J. Attardi写了一个不错的jQuery插件&#xff0c;只是这样。它比罗伯特的更稳定&#xff0c;并且当场得到焦点时&#xff0c;还会变淡到更浅的灰色。我修改他的插件读取占位符属性&#xff0c;而不是手动创建跨度。This fiddle具有完整代码&#xff1a;HTMLJS// Origin…

spring AspectJ的Execution表达式

Aspectj切入点语法定义 在使用spring框架配置AOP的时候&#xff0c;不管是通过XML配置文件还是注解的方式都需要定义pointcut"切入点" 例如定义切入点表达式 execution(* com.sample.service.impl..*.*(..)) execution()是最常用的切点函数&#xff0c;其语法如下所示…

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

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

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

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

MspEmu W.I.P.

转载于:https://www.cnblogs.com/sesexxoo/archive/2006/07/29/6190717.html