linux 内核异步预读,linux内核之异步IO

一、前言

在嵌入式linux中,除了前面讲到的轮询式IO还有异步IO。异步IO可以在驱动或者文件在处理某一件事情后再想用户空间发出信号,使得应用程序可以不用阻塞或者轮序去做其他事情,知道信号发生后再来处理。这样可以使得我们的应用程序更加灵活,它与轮询IO互为补充。本文着重讲一下异步IO信号的原理,结合简单的应用程序及驱动程序来讲解

二、信号

2.1、可靠信号与不可靠信号

异步IO 有一种常用的实现方式,就是信号。在linux操作系统中,信号一共有 30 个。在PC端的linux中,有些发行版的信号有 64 个,并且分为可靠信号与不可靠信号。其中小于 SIGRTMIN=32 的为不可靠信号,而大于SIGRTMIN并且小于 SIGRTMAX=63 的为可靠信号。

我们可以使用下面的命令来查看操作系统支持的信号,如果所示

kill -l

89239aeaa4d2

操作系统信号

那么什么是 **可靠信号 **与 不可靠信号 ?

在执行 信号处理程序 时,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

#include

#include

#include

#include

#include

#include

#include

#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的异步信号机制有了更深一层的了解

三、参考链接

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值