一、前言
在嵌入式linux中,除了前面讲到的轮询式IO还有异步IO。异步IO可以在驱动或者文件在处理某一件事情后再想用户空间发出信号,使得应用程序可以不用阻塞或者轮序去做其他事情,知道信号发生后再来处理。这样可以使得我们的应用程序更加灵活,它与轮询IO互为补充。本文着重讲一下异步IO信号的原理,结合简单的应用程序及驱动程序来讲解
二、信号
2.1、可靠信号与不可靠信号
异步IO 有一种常用的实现方式,就是信号。在linux操作系统中,信号一共有 30 个。在PC端的linux中,有些发行版的信号有 64 个,并且分为可靠信号与不可靠信号。其中小于 SIGRTMIN=32 的为不可靠信号,而大于SIGRTMIN并且小于 SIGRTMAX=63 的为可靠信号。
我们可以使用下面的命令来查看操作系统支持的信号,如果所示
kill -l

那么什么是 **可靠信号 **与 不可靠信号 ?
在执行 信号处理程序 时,linux默认不再接收当前正在处理的信号。所以此时如果内核再次发出信号时,那么会被应用程序忽略掉。这样的信号我们称之为不可靠信号,因为造成了信号丢失。可靠信号则不会丢失,因为可靠信号会被加入信号队列,在系统处理完信号之后再次发出,每一次都会被接收到,不造成信号丢失的现象
关于可靠信号 和 不可靠信号 的详情,请各位读者从其他文章资料获取
2.2、信号应用
2.2.1、信号常用接口
我们在应用层一般情况下有 2 种使用信号的方法,分别是:
- sighandler_t signal(int signum, sighandler_t handler)
- int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
前者的操作比较简单,只是为某一个信号注册 处理函数。而后者可用于改变进程接收到信号后的行为,但其使用复杂度也比前者高一些,其中 struct sigaction 结构体如下,我们后面还会再说到该结构体。
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
在设置信号后,我们还需要使用 fcntl 系统调用会相关的驱动或者文件进行一些操作,常用的有
-
F_SETOWN:一般是使用语句 fcntl(common_fd, F_SETOWN, getpid()),对于某些多进程共用的文件描述符,比如标准输入输出,我们要让操作系统知道这些信号要发往哪个进程。因为每一个进程都有标准输入输出,所以我们需要让操作系统知道当前的标准输入输出属于哪个进程,从而可以对进程发送信号
-
F_SETFL:一般是使用语句 fcntl(your_fd, F_SETFL, old_flags | FASYNC),该语句让对应的驱动或者文件启动异步通知机制
-
F_SETSIG:该标志可以设置用户的 自定义信号 来替换掉原本的信号 SIGIO。一般的使用场景是多线程下,如果多个线程使用 SIGIO 作为触发信号且每个线程对该信号的处理函数都不相同,那么这样会造成 SIGIO 处理函数的覆盖,最终只有一个处理函数会被执行。从中可以看出,信号是 进程 属性,也就是在进程范围内,一个信号只有一个处理函数。那么我们可以调用该接口为每个线程指定一个自定义信号来替代 SIGIO,并为自定义信号安装处理函数。那么当驱动或者文件触发 SIGIO 信号时,从线程角度来看则是触发了每个线程自己的自定义信号,那么此时就会执行每个线程自己的处理函数。使用该标志时需要加入编译选项** -D_GNU_SOURCE**,不然会出现 F_SETSIG undeclared 错误
-
F_SETOWN_EX:当我们想要让驱动或文件的触发信号只发送到某一 指定线程 。那么我们就可以使用该标志来设置信号的接收线程。它与 F_SETOWN 的区别在 F_SETOWN_EX 更加细致,可以指定只发送给某个线程;而 F_SETOWN 优先发送给线程,如果接收线程被阻塞,则选择同一进程中的其他线程接收。
以上就是笔者总结出来的 应用层信号 使用方法,信号还有很多其他的高级用法,这里笔者暂时未做深入研究,有兴趣的读者可以自行查阅其他资料,后续笔者有机会再把坑给填上
下面的笔者应用层样例代码,有需要的读者可以借鉴。每个人的内核驱动都不同,读者们可以自行实现内核驱动后来使用该样例代码验证
#include <stdio.h>
#include <sys/epoll.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <strings.h>
#include <signal.h>
#include <sys/types.h>
#define SIGTEST (SIGRTMIN+1)
void test_sigacftionHandle(int signum , siginfo_t* siginfo, void* NULL_ptr)
{
/* 在非实时信号下 si_code一直等于128,只有在实时信号下才是内核发送出来的值 */
printf("si_code = %d, si_band = %ld\n", siginfo->si_code, siginfo->si_band);
}
void test_signalHandle(int signum)
{
printf("signum = %ld\n", signum);
}
int main()
{
/* 非实时信号的正常sigaction流程 */
int fd = 0;
int old_flags = 0;
struct sigaction sig_act = {0};
fd = open("/dev/gpio_device", O_NONBLOCK);
fcntl(fd, F_SETOWN, getpid());
old_flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, old_flags | FASYNC);
sig_act.sa_flags = SA_SIGINFO;
sig_act.sa_sigaction = test_sigacftionHandle;
sigaction(SIGIO, &sig_act, NULL);//这样写会提示Real-time signal 1
while(1)
sleep(1);
return 0;
/* 非实时信号的正常signal流程 */
int fd = 0;
int old_flags = 0;
fd = open("/dev/gpio_device", O_NONBLOCK);
fcntl(fd, F_SETOWN, getpid());
old_flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, old_flags | FASYNC);
signal(SIGIO, test_sigHandle);
while(1)
sleep(1);
return 0;
/* 使用signal安装sa_sigaction类型的函数会编译错误 */
int fd = 0;
int old_flags = 0;
fd = open("/dev/gpio_device", O_NONBLOCK);
fcntl(fd, F_SETOWN, getpid());
old_flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, old_flags | FASYNC);
signal(SIGIO, test_sigacftionHandle);
while(1)
sleep(1);
return 0;
/* 设置实时信号后,为SIGIO安装处理函数。当信号发生是会出现 Real-time signal 1 ,并退出程序*/
int fd = 0;
int old_flags = 0;
struct sigaction sig_act = {0};
fd = open("/dev/gpio_device", O_NONBLOCK);
fcntl(fd, F_SETISG, SIGTEST);
fcntl(fd, F_SETOWN, getpid());
old_flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, old_flags | FASYNC);
sig_act.sa_flags = SA_SIGINFO;
sig_act.sa_sigaction = test_sigacftionHandle;
sigaction(SIGIO, &sig_act, NULL);//这样写会提示
while(1)
sleep(1);
return 0;
/* 实时信号的正常signal流程 */
int fd = 0;
int old_flags = 0;
struct sigaction sig_act = {0};
fd = open("/dev/gpio_device", O_NONBLOCK);
fcntl(fd, F_SETISG, SIGTEST);
fcntl(fd, F_SETOWN, getpid());
old_flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, old_flags | FASYNC);
sig_act.sa_flags = SA_SIGINFO;
sig_act.sa_sigaction = test_sigacftionHandle;
sigaction(SIGTEST, &sig_act, NULL);//正常
while(1)
sleep(1);
return 0;
}
2.3、驱动层信号
应用程序 是接收信号的,那么发送信号的则是 内核驱动。在驱动层面,linux提供了 2 个接口来实现信号的发送,分别是:
- int fasync_helper(int fd, struct file* filp, int on, struct fasync_struct **fapp)
- void kill_fasync(struct fasync_struct **fp, int sig, int band)
2.3.1 fasync_helper
fasync_helper
->fasync_remove_entry or fasync_add_entry
下面是 fasync_helper 相关源码解析部分,其中已经把部分代码给省略去,以简化讲解思路。代码 注释 就是讲解内容
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
{
if (!on)
return fasync_remove_entry(filp, fapp);
return fasync_add_entry(fd, filp, fapp);
}
int fasync_remove_entry(struct file *filp, struct fasync_struct **fapp)
{
struct fasync_struct *fa, **fp;
int result = 0;
....
for (fp = fapp; (fa = *fp) != NULL; fp = &fa->fa_next) {
if (fa->fa_file != filp)
continue;//一直循环,直到找到fapp所指向相应的struct fasync_struct结构
....
*fp = fa->fa_next;//将fapp前后的元素连接起来,其中fa指向当前的元素,fp是个双重指针,指向了一个元素的next成员的地址
call_rcu(&fa->fa_rcu, fasync_free_rcu);//释放当前的struct fasync_struct结构
result = 1;
break;
}
spin_unlock(&fasync_lock);
spin_unlock(&filp->f_lock);
return result;
}
static int fasync_add_entry(int fd, struct file *filp, struct fasync_struct **fapp)
{
struct fasync_struct *new;
new = fasync_alloc();//为新结构开辟内存
if (!new)
return -ENOMEM;
if (fasync_insert_entry(fd, filp, fapp, new)) {//将新结构加入链表
fasync_free(new);//如果新结构加入队列失败则释放掉
return 0;
}
return 1;
}
struct fasync_struct *fasync_insert_entry(int fd, struct file *filp, struct fasync_struct **fapp, struct fasync_struct *new)
{
struct fasync_struct *fa, **fp;
....
for (fp = fapp; (fa = *fp) != NULL; fp = &fa->fa_next) {//查找链表是否存在当前文件的struct fasync_struct结构体
if (fa->fa_file != filp)//如果当前不是则继续遍历下一个
continue;
spin_lock_irq(&fa->fa_lock);//找到当前的struct fasync_struct结构体,更换文件描述符
fa->fa_fd = fd;
spin_unlock_irq(&fa->fa_lock);
goto out;
}
//跳出循环则说明链表没有当前文件的struct fasync_struct结构体,将新开辟的struct fasync_struct结构体加入链表中,并设置标志位FASYNC
spin_lock_init(&new->fa_lock);
new->magic = FASYNC_MAGIC;
new->fa_file = filp;
new->fa_fd = fd;
new->fa_next = *fapp;
rcu_assign_pointer(*fapp, new);
filp->f_flags |= FASYNC;
out:
spin_unlock(&fasync_lock);
spin_unlock(&filp->f_lock);
return fa;
}
-
fasync_helper
我们看首先看到 fasync_helper 这个函数,该函数功能就是让当前进程进入或离开struct fasync_struct 结构体队列(为了方面下面将struct fasync_struct 结构体简称为fa结构体)。而第三个参数 on 就是决定进程是 进入链表 还是 离开链表。而我们传给该函数只需要一个指针,该指针就是 链表头。 -
fasync_add_entry
该函数先使用 fasync_alloc 开辟了一个 fa结构体,然后讲该结构体指针传入 fasync_insert_entry,注意该函数的参数struct fasync_struct **fapp,它是个二级指针,指向了我们传入给 fasync_helper 的fa结构体指针,先在链表上进行一次遍历,如果找到链表上有当前进程传入的 fa结构体。如果没有遍历到后就跳出循环,将参数 fapp 赋值为新开辟的结构体指针,这样我们完成了结构体入链的过程了,而我们传入的二级指针也指向了一个 fa结构体 -
fasync_remove_entry
该函数是让当前进程的 fa结构体 离开链表,同理也是对链表进行遍历,如果发现当前进程有 fa结构体 链表中,就将结构体出链并释放。这里我们要注意到 变量fp 也是一个二级指针,该指针指向了我们传入的 fp指针 或者 fa结构体的fa_next成员 。变量fa指针 指向当前遍历到的节点,节点使用fa结构体指针来表示,我们从代码中可以看到 fa 与 *fp 都指向了同一个内存地址,但是我们也要注意到 fp 这个指针指向了上一个节点的fa_next成员,所以这里其实就是将上一个节点的fa_next成员 指向 下一个节点的地址,这样就实现了节点出链。这里的上一个节点和下一个节点都是相对当前节点而言。这里的逻辑可能比较绕,需要各位读者仔细观察思考
2.3.1 kill_fasync
kill_fasync 稍显复杂,我们先看一下调用关系和阅读相关源码,然后再往下看一下讲解。同理,这里笔者也省略了部分代码以简化讲解思路
kill_fasync
->send_sigio
->send_sigio_to_task
->do_send_sig_info
void kill_fasync(struct fasync_struct **fp, int sig, int band)
{
/* First a quick test without locking: usually
* the list is empty.
*/
if (*fp) {
rcu_read_lock();
kill_fasync_rcu(rcu_dereference(*fp), sig, band);
rcu_read_unlock();
}
}
static void kill_fasync_rcu(struct fasync_struct *fa, int sig, int band)
{
while (fa) {//查看struct fasync_struct结构体是否有效
struct fown_struct *fown;
unsigned long flags;
....
spin_lock_irqsave(&fa->fa_lock, flags);
if (fa->fa_file) {
fown = &fa->fa_file->f_owner;
if (!(sig == SIGURG && fown->signum == 0))//sig并没有继续往下传递,只是在这里作为判断用
send_sigio(fown, fa->fa_fd, band);//向应用空间发送信号
}
spin_unlock_irqrestore(&fa->fa_lock, flags);
fa = rcu_dereference(fa->fa_next);//遍历下一个struct fasync_struct结构体,这样就把所谓在这个链表上的进程都遍历了一遍,对每一个使用了该设备异步通知方法的进程都发送了信号
}
}
void send_sigio(struct fown_struct *fown, int fd, int band)
{
struct task_struct *p;
enum pid_type type;
struct pid *pid;
int group = 1;
....
pid = fown->pid;
if (!pid)//如果pid为空则不进行发送,所以要发送信号必须在应用层使用F_SETOWN
goto out_unlock_fown;
do_each_pid_task(pid, type, p) {//这里按笔者 的理解是对该进程的所有线程都发送信号
send_sigio_to_task(p, fown, fd, band, group);
} while_each_pid_task(pid, type, p);
....
out_unlock_fown:
read_unlock(&fown->lock);
}
static void send_sigio_to_task(struct task_struct *p,
struct fown_struct *fown,
int fd, int reason, int group)
{
int signum = ACCESS_ONCE(fown->signum);
if (!sigio_perm(p, fown, signum))
return;
switch (signum) {
siginfo_t si;
default:
/* Queue a rt signal with the appropriate fd as its
value. We use SI_SIGIO as the source, not
SI_KERNEL, since kernel signals always get
delivered even if we can't queue. Failure to
queue in this case _should_ be reported; we fall
back to SIGIO in that case. --sct */
/*这里是意思是说如果一个实时信号(信号值大于32)无法进队信号队里,
那么我们需要报告这件事情,那么报告就需要发送信号,这个信号就是SIGIO*/
si.si_signo = signum;
si.si_errno = 0;
si.si_code = reason;
/*
* Posix definies POLL_IN and friends to be signal
* specific si_codes for SIG_POLL. Linux extended
* these si_codes to other signals in a way that is
* ambiguous if other signals also have signal
* specific si_codes. In that case use SI_SIGIO instead
* to remove the ambiguity.
*/
//如果发送的信号不是SIGPOLL且有指定的si_code时,此时si_code会被指定为SI_SIGIO,一般信号不会有指定的si_code
if ((signum != SIGPOLL) && sig_specific_sicodes(signum))
si.si_code = SI_SIGIO;
/* Make sure we are called with one of the POLL_*
reasons, otherwise we could leak kernel stack into
userspace. */
BUG_ON((reason < POLL_IN) || ((reason - POLL_IN) >= NSIGPOLL));
if (reason - POLL_IN >= NSIGPOLL)
si.si_band = ~0L;
else
si.si_band = band_table[reason - POLL_IN];
si.si_fd = fd;
if (!do_send_sig_info(signum, &si, p, group))//当发送信号失败时,我们就不进行break,而是跳到了case 0去执行,从而达到了失败就发送SIGIO的目的
break;
/* fall-through: fall back on the old plain SIGIO signal */
case 0:
do_send_sig_info(SIGIO, SEND_SIG_PRIV, p, group);//通过发送SIGIO,让用户程序知道实时信号入队失败
}
}
-
kill_fasync
我们先看看该函数的参数,除了 fa结构体 之外。还有 sig 和 band,sig 我们知道就是信号值,其实该值并不是我们发送到应用层的值,它的作用只是做一个检查而已,我们在后面会再看到。但我们要注意这个 band ,我们在后面会提起他的作用 -
kill_fasync_rcu
该函数是一个 while 循环,可以从循环中看出每一次都会判断 fa结构体是否有效,且在循环完成后会遍历一 一个 fa结构体,从而达到发送信号给每一个挂在 fa结构体链表 上的进程。它主要就是做一些逻辑判断,很明显,该接口不允许发送 SIGURG 信号。然后直接调用 send_sigio。注意这里传入了参数 band,但是参数 **sig 并没有往下继续传递,进而还是用于逻辑判断 -
send_sigio
该函数更加简单,就是一个 for_each 的循环。按照笔者的理解,该循环是对进程上的每一个线程都执行一次 send_sigio_to_task。这里还需要注意,这里回判断 fown->pid 是否为空,非空情况下才发送,不然则跳过循环直接退出,如果要设置在成员,则必须在用户空间使用F_SETOWN。循环部分不是本文的主要内容,有兴趣的读者可以翻阅代码
!!!这里需要注意到,每一个进程打开同一个设备文件时,都会生成不同的struct file结构体 -
send_sigio_to_task
该函数是本节的 主要内容。其中参数 reason 就是我们前面说的参数 band。那么这里笔者需要先说到另外的知识点,也就是第一小节应用层信号提到的可靠信号和不可靠信号以及F_SETSIG标志。那么可靠信号的范围是SIGRTMIN < sig_value < SIGRTMAX。按照笔者的理解,可靠信号也称为实时信号。
4.1. case 0分支
笔者为什么要提到这个呢?我们在前面说参数 sig 并没有往下传递,那么我们往应用层发送的信号值从哪里来?代码很明显给我们答案了,其实他就是来自fown结构体的 signum 成员,该成员就是我们使用F_SETSIG标志设置的信号值,如果我们没有使用该标志进行设置,那么默认发送SIGIO信号,也就是执行 case 0 的分支。
4.2. default分支
那么如果我们在应用层使用了 F_SETSIG标志 标志设置了信号值,该信号值的范围一般是SIGRTMIN < sig_value < SIGRTMAX,也就是实时信号。那么 fown->signum 就会变成我们指定的信号值。那么此时函数会执行 default分支。在该分支中,如果发送的信号不是 SIGPOLL 且有指定的si_code时,此时si_code会被指定为SI_SIGIO(一般信号不会有指定的si_code),那么参数 reason 会被赋值给 siginfo_t结构体 的 si_code成员 ,而且该siginfo_t结构体也会被我们发往应用层,让应用程序接收。后面笔者会说一下如何在应用层接收该结构体。那么我们的参数 band 就这样被发送往应用程了,这样我们就可以在应用层读取该值,知道驱动发生的异步事件是哪个种类的事件,比如是读事件 还是 写事件。这样我们在应用层编程就更加灵活。当然了,仅限于使用了F_SETSIG标志为 进程 或 线程 设置了实时信号。
4.3. 从default分支到case 0分支
我们在看看 default分支 的 break语句,会发现该语句有条件才会触发的,也就是函数 do_send_sig_info 返回 0 才触发。在 linux 中,返回 0 一般是表示执行成功。那么如果是返回非 0 值,表示不成功,那么按照C语言的语法,这个时候会往下执行,也就是执行 case 0分支发送 SIGIO 信号。这是为什呢?按照笔者的理解,并不是所有实时信号都能够成功发送,当内核信号队列满了,那么信号就有可能入队不成功,也就无法送往应用层。那么应用层此时需要知道信号到底有没有发送成功,那么我们就是通过使用 SIGIO 来通知应用层实时信号发送失败。那么这个逻辑的应用场景笔者目前没有遇到,但我们知道了这样的事情,在我们遇到特殊场景的时候也许会有用
按照笔者的理解,讲到这里应该可以理解异步信号机制的大部分了吧。那么关于驱动层面就讲得差不多了,有兴趣的读者可以继续往下阅读代码。
2.3、接收siginfo_t结构体
typedef struct siginfo_t{
int si_signo;//信号编号
int si_errno;//如果为非零值则错误代码与之关联
int si_code;//说明进程如何接收信号以及从何处收到
pid_t si_pid;//适用于SIGCHLD,代表被终止进程的PID
pid_t si_uid;//适用于SIGCHLD,代表被终止进程所拥有进程的UID
int si_status;//适用于SIGCHLD,代表被终止进程的状态
clock_t si_utime;//适用于SIGCHLD,代表被终止进程所消耗的用户时间
clock_t si_stime;//适用于SIGCHLD,代表被终止进程所消耗系统的时间
sigval_t si_value;
int si_int;
void * si_ptr;
void* si_addr;
int si_band;
int si_fd;
};
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
在我们使用 sigaction 接口的时候,我们需要传入相应信号的 struct sigaction 结构体,其成员说明如下:
-
_sa_handler处理函数只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息
-
_sa_sigaction处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值,第三个参数没有使用(posix没有规范使用该参数的标准)
-
sa_mask 指定在信号处理程序的 执行过程中,哪些信号应当被阻塞。缺省情况下当前信号本身被阻塞,防止信号的嵌套发送。除非指定 SA_NODEFER 或者 SA_NOMASK标志位,那么在处理程序执行完后,被阻塞的信号开始执行。
-
sa_flags 中包含了许多标志位,包括刚刚提到的 SA_NODEFER 及 SA_NOMASK 标志位。另一个重要的标志位是 SA_SIGINFO。当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,也就是 siginfo_t结构体 会被传入。因此,如果此时设置了 SA_SIGINFO标志,那么应该为sa_sigaction函数指针赋值。如果 不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将 导致段错误(Segmentation fault)。
那么到了这里,各位读者应该知道获取 siginfo_t结构体 了。希望通过此文,可以让各位读者对于linux的异步信号机制有了更深一层的了解
三、参考链接
信号发送函数sigqueue和信号安装函数sigaction: https://www.cnblogs.com/mickole/p/3191804.html
异步信号SIGIO为什么会被截胡?https://www.cnblogs.com/arnoldlu/p/10185126.html
可靠信号与不可靠信号https://www.cnblogs.com/wsw-seu/p/8383737.html
应用层获得SIGIO信号如何区分是kill_fasync的第3个参数https://bbs.csdn.net/topics/392292366
IO多路复用、信号驱动IO以及epollhttps://www.cnblogs.com/arnoldlu/p/10264350.html
网友评论