linux内核之异步IO

2019-12-30  本文已影响0人  wipping的技术小栈

一、前言

在嵌入式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 种使用信号的方法,分别是:

前者的操作比较简单,只是为某一个信号注册 处理函数。而后者可用于改变进程接收到信号后的行为,但其使用复杂度也比前者高一些,其中 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 系统调用会相关的驱动或者文件进行一些操作,常用的有

以上就是笔者总结出来的 应用层信号 使用方法,信号还有很多其他的高级用法,这里笔者暂时未做深入研究,有兴趣的读者可以自行查阅其他资料,后续笔者有机会再把坑给填上

下面的笔者应用层样例代码,有需要的读者可以借鉴。每个人的内核驱动都不同,读者们可以自行实现内核驱动后来使用该样例代码验证

#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 个接口来实现信号的发送,分别是:

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;
}
  1. fasync_helper
    我们看首先看到 fasync_helper 这个函数,该函数功能就是让当前进程进入或离开struct fasync_struct 结构体队列为了方面下面将struct fasync_struct 结构体简称为fa结构体)。而第三个参数 on 就是决定进程是 进入链表 还是 离开链表。而我们传给该函数只需要一个指针,该指针就是 链表头
  2. fasync_add_entry
    该函数先使用 fasync_alloc 开辟了一个 fa结构体,然后讲该结构体指针传入 fasync_insert_entry,注意该函数的参数struct fasync_struct **fapp,它是个二级指针,指向了我们传入给 fasync_helperfa结构体指针,先在链表上进行一次遍历,如果找到链表上有当前进程传入的 fa结构体。如果没有遍历到后就跳出循环,将参数 fapp 赋值为新开辟的结构体指针,这样我们完成了结构体入链的过程了,而我们传入的二级指针也指向了一个 fa结构体
  3. 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,让用户程序知道实时信号入队失败
    }
}
  1. kill_fasync
    我们先看看该函数的参数,除了 fa结构体 之外。还有 sigbandsig 我们知道就是信号值,其实该值并不是我们发送到应用层的值,它的作用只是做一个检查而已,我们在后面会再看到。但我们要注意这个 band ,我们在后面会提起他的作用
  2. kill_fasync_rcu
    该函数是一个 while 循环,可以从循环中看出每一次都会判断 fa结构体是否有效,且在循环完成后会遍历一 一个 fa结构体,从而达到发送信号给每一个挂在 fa结构体链表 上的进程。它主要就是做一些逻辑判断,很明显,该接口不允许发送 SIGURG 信号。然后直接调用 send_sigio。注意这里传入了参数 band,但是参数 **sig 并没有往下继续传递,进而还是用于逻辑判断
  3. send_sigio
    该函数更加简单,就是一个 for_each 的循环。按照笔者的理解,该循环是对进程上的每一个线程都执行一次 send_sigio_to_task。这里还需要注意,这里回判断 fown->pid 是否为空,非空情况下才发送,不然则跳过循环直接退出,如果要设置在成员,则必须在用户空间使用F_SETOWN。循环部分不是本文的主要内容,有兴趣的读者可以翻阅代码
    !!!这里需要注意到,每一个进程打开同一个设备文件时,都会生成不同的struct file结构体
  4. 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 结构体,其成员说明如下:

那么到了这里,各位读者应该知道获取 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

上一篇 下一篇

猜你喜欢

热点阅读