按键驱动:异步通知机制

一、相关函数说明

signal函数

signum是传递给它的唯一参数。执行了signal()调用后,进程只要接收到类型为signum的信号,不管其正在执行程序的哪一部分,就立即执行handler()函数。当handler()函数执行结束后,控制权返回进程被中断的那一点继续执行。

函数原型:

1
2
3
4
void (*signal(int signum,void(* handler)(int)))(int);
//或者:
typedef void (*sig_t)( int );
sig_t signal(int signum,sig_t handler);

参数说明

  • signum:所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。

  • handler:与信号关联的动作,它可以取以下三种值:

    • 一个无返回值的函数地址

      此函数必须在signal()被调用前申明,handler中为这个函数的名字。当接收到一个类型为signum的信号时,就执行handler 所指定的函数。这个函数应有如下形式的定义:

      1
      void func(int sig);
    • SIG_IGN

      这个符号表示忽略该信号,执行了相应的signal()调用后,进程会忽略类型为sig的信号。

    • SIG_DFL

      这个符号表示恢复系统对信号的默认处理。

**返回值:**返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1)。

函数说明

signal()会依参数signum 指定的信号编号来设置该信号的处理函数。当指定的信号到达时就会跳转到参数handler指定的函数执行。当一个信号的信号处理函数执行时,如果进程又接收到了该信号,该信号会自动被储存而不会中断信号处理函数的执行,直到信号处理函数执行完毕再重新调用相应的处理函数。但是如果在信号处理函数执行时进程收到了其它类型的信号,该函数的执行就会被中断。

下面的情况可以产生Signal:

  1. 按下CTRL+C产生SIGINT
  2. 硬件中断,如除0,非法内存访问(SIGSEV)等等
  3. Kill函数可以对进程发送Signal
  4. Kill命令。实际上是对Kill函数的一个包装
  5. 软件中断。如当Alarm Clock超时(SIGURG),当Reader中止之后又向管道写数据(SIGPIPE),等等

相关的信号:

Signal Description
SIGABRT 由调用abort函数产生,进程非正常退出
SIGALRM 用alarm函数设置的timer超时或setitimer函数设置的interval timer超时
SIGBUS 某种特定的硬件异常,通常由内存访问引起
SIGCANCEL 由Solaris Thread Library内部使用,通常不会使用
SIGCHLD 进程Terminate或Stop的时候,SIGCHLD会发送给它的父进程。缺省情况下该Signal会被忽略
SIGCONT 当被stop的进程恢复运行的时候,自动发送
SIGEMT 和实现相关的硬件异常
SIGFPE 数学相关的异常,如被0除,浮点溢出,等等
SIGFREEZE Solaris专用,Hiberate或者Suspended时候发送
SIGHUP 发送给具有Terminal的Controlling Process,当terminal被disconnect时候发送
SIGILL 非法指令异常
SIGINFO BSD signal。由Status Key产生,通常是CTRL+T。发送给所有Foreground Group的进程
SIGINT 由Interrupt Key产生,通常是CTRL+C或者DELETE。发送给所有ForeGround Group的进程
SIGIO 异步IO事件
SIGIOT 实现相关的硬件异常,一般对应SIGABRT
SIGKILL 无法处理和忽略。中止某个进程
SIGLWP 由Solaris Thread Libray内部使用
SIGPIPE 在reader中止之后写Pipe的时候发送
SIGPOLL 当某个事件发送给Pollable Device的时候发送
SIGPROF Setitimer指定的Profiling Interval Timer所产生
SIGPWR 和系统相关。和UPS相关。
SIGQUIT 输入Quit Key的时候(CTRL+\)发送给所有Foreground Group的进程
SIGSEGV 非法内存访问
SIGSTKFLT Linux专用,数学协处理器的栈异常
SIGSTOP 中止进程。无法处理和忽略。
SIGSYS 非法系统调用
SIGTERM 请求中止进程,kill命令缺省发送
SIGTHAW Solaris专用,从Suspend恢复时候发送
SIGTRAP 实现相关的硬件异常。一般是调试异常
SIGTSTP Suspend Key,一般是Ctrl+Z。发送给所有Foreground Group的进程
SIGTTIN 当Background Group的进程尝试读取Terminal的时候发送
SIGTTOU 当Background Group的进程尝试写Terminal的时候发送
SIGURG 当out-of-band data接收的时候可能发送
SIGUSR1 用户自定义signal 1
SIGUSR2 用户自定义signal 2
SIGVTALRM setitimer函数设置的Virtual Interval Timer超时的时候
SIGWAITING Solaris Thread Library内部实现专用
SIGWINCH 当Terminal的窗口大小改变的时候,发送给Foreground Group的所有进程
SIGXCPU 当CPU时间限制超时的时候
SIGXFSZ 进程超过文件大小限制
SIGXRES Solaris专用,进程超过资源限制的时候发送

注意:

1、不要使用低级的或者STDIO.H的IO函数

2、不要使用对操作

3、不要进行系统调用

4、不是浮点信号的时候不要用longjmp

5、signal函数是由ISO C定义的。因为ISO C不涉及多进程,进程组以及终端I/O等,所以他对信号的定义非常含糊,以至于对UNIX系统而言几乎毫无用处。

备注:因为signal的语义与现实有关,所以最好使用sigaction函数替代本函数。


fcntl函数

通过fcntl可以改变已打开的文件性质。fcntl针对描述符提供控制。参数fd是被参数cmd操作的描述符。针对cmd的值,fcntl能够接受第三个参数int arg。

fcntl的返回值与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。

函数原型:

1
2
3
int fcntl(int fd, int cmd); 
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);

参数说明

  • fd:欲设置的文件描述符。
  • cmd:打算操作的指令,它可以取以下几种值:
    • F_DUPFD用来查找大于或等于参数arg的最小且仍未使用的文件描述符,并且复制参数fd的文件描述符。执行成功则返回新复制的文件描述符。新描述符与fd共享同一文件表项,但是新描述符有它自己的一套文件描述符标志,其中FD_CLOEXEC文件描述符标志被清除。请参考dup2。
    • F_GETFD取得close-on-exec旗标。若此旗标的FD_CLOEXEC位为0,代表在调用exec()相关函数时文件将不会关闭。
    • F_SETFD 设置close-on-exec 旗标。该旗标以参数arg 的FD_CLOEXEC位决定。
    • F_GETFL 取得文件描述符状态旗标,此旗标为open()的参数flags。
    • F_SETFL 设置文件描述符状态旗标,参数arg为新旗标,但只允许O_APPEND、O_NONBLOCK和O_ASYNC位的改变,其他位的改变将不受影响。
    • F_GETLK 取得文件锁定的状态。
    • F_SETLK 设置文件锁定的状态。此时flcok 结构的l_type 值必须是F_RDLCK、F_WRLCK或F_UNLCK。如果无法建立锁定,则返回-1,错误代码为EACCES 或EAGAIN。
    • F_SETLKW F_SETLK 作用相同,但是无法建立锁定时,此调用会一直等到锁定动作成功为止。若在等待锁定的过程中被信号中断时,会立即返回-1,错误代码为EINTR。

**返回值:**与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。


kill_fasync函数

当设备可写/读时,函数kill_fasync会发送信号sig给内核。

函数原型:

1
void kill_fasync(struct fasync_struct **fp, int sig, int band)

参数说明

  • fp:内核的异步队列
  • sig:所要发送的信号类型。
  • band:带宽,它可以取以下两种值:
    • POLL_IN:设备可读
    • POLL_OUT:设备可写

二、Linux异步通知

1. 什么是异步通知

个人认为,异步通知类似于中断的机制,如下面的将要举例的程序,当设备可写时,设备驱动函数发送一个信号给内核,告知内核有数据可读,在条件不满足之前,并不会造成阻塞。而不像之前学的阻塞型IO和poll,它们是调用函数进去检查,条件不满足时还会造成阻塞。

2. 应用层中启用异步通知机制

其实就三个步骤:

1)

1
signal(SIGIO, sig_handler);

调用signal函数,让指定的信号SIGIO与处理函数sig_handler对应。

2)

1
fcntl(fd, F_SET_OWNER, getpid());

指定一个进程作为文件的“属主(filp->owner)”(可以理解为告诉驱动程序应用程序的PID),这样内核才知道信号要发给哪个进程。

3)

1
2
f_flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, f_flags | FASYNC);

在设备文件中添加FASYNC标志,驱动中就会调用驱动中实现的fasync函数。

三个步骤执行后,一旦有信号产生,相应的应用程序中的进程就会收到。

3. 驱动中需要实现的异步通知

实现异步通知,内核需要知道几个东西:哪个文件(filp),什么信号(SIGIO),发给哪个进程(pid),收到信号后做什么(sig_handler)。这些都由上述前两个步骤完成了,而这前两个步骤内核帮忙实现了,所以,我们只需要实现第三个步骤的一个简单的传参。

  • fasync_struct结构体

    要实现传参,我们需要把一个结构体struct fasync_struct添加到内核的异步队列中,这个结构体用来存放对应设备文件的信息(如fd, filp)并交给内核来管理。一但收到信号,内核就会在这个所谓的异步队列头找到相应的文件(fd),并在filp->owner中找到对应的进程PID,并且调用对应的sig_handler了。

    1
    2
    3
    4
    5
    6
    struct fasync_struct {
    int magic;
    int fa_fd;
    struct fasync_struct *fa_next; /* singly linked list */
    struct file *fa_file;
    };

上面说了前两个步骤会由内核完成,所以我们只要做两件事情:

  1. 定义结构体fasync_struct。

    1
    struct fasync_struct *async_queue;
  2. 实现驱动程序中的fasync函数:test_fasync,利用函数fasync_helper将fd,filp和定义的异步队列结构体传给内核(这里主要是初始化异步队列async_queue)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    static struct fasync_struct *async_queue;

    int test_fasync (int fd, struct file *filp, int on)
    {
    return fasync_helper(fd, filp, on, &async_queue);
    }

    static struct file_operations sencod_drv_fops = {
    .owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    ...
    ...
    .fasync = test_fasync,
    };

    函数fasync_helper的定义为:

    1
    int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)

    该函数主要负责初始化或释放异步队列async_queue。前面的三个参数其实就是teat_fasync的三个参数,所以只要我们定义好的fasync_struct结构体也传进去就可以了。

  3. 当设备可读时,调用函数kill_fasync发送信号SIGIO给内核。

    1
    kill_fasync (&async_queue, SIGIO, POLL_IN);
  4. 当设备关闭时,需要将fasync_struct从异步队列中删除:

    1
    test_fasync(-1, filp, 0);

    删除也是调用test_fasync,不过改了一下参数而已,实际上是通过fasync_helper函数实现的