按键驱动:异步通知机制
按键驱动:异步通知机制
一、相关函数说明
signal函数
signum是传递给它的唯一参数。执行了signal()调用后,进程只要接收到类型为signum的信号,不管其正在执行程序的哪一部分,就立即执行handler()函数。当handler()函数执行结束后,控制权返回进程被中断的那一点继续执行。
函数原型:
1 | void (*signal(int signum,void(* handler)(int)))(int); |
参数说明:
-
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:
- 按下CTRL+C产生SIGINT
- 硬件中断,如除0,非法内存访问(SIGSEV)等等
- Kill函数可以对进程发送Signal
- Kill命令。实际上是对Kill函数的一个包装
- 软件中断。如当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 | int fcntl(int fd, int cmd); |
参数说明:
- 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 | f_flags = fcntl(fd, F_GETFL); |
在设备文件中添加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
6struct fasync_struct {
int magic;
int fa_fd;
struct fasync_struct *fa_next; /* singly linked list */
struct file *fa_file;
};
上面说了前两个步骤会由内核完成,所以我们只要做两件事情:
-
定义结构体fasync_struct。
1
struct fasync_struct *async_queue;
-
实现驱动程序中的fasync函数:test_fasync,利用函数fasync_helper将fd,filp和定义的异步队列结构体传给内核(这里主要是初始化异步队列async_queue)。
1
2
3
4
5
6
7
8
9
10
11
12
13static 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结构体也传进去就可以了。
-
当设备可读时,调用函数kill_fasync发送信号SIGIO给内核。
1
kill_fasync (&async_queue, SIGIO, POLL_IN);
-
当设备关闭时,需要将fasync_struct从异步队列中删除:
1
test_fasync(-1, filp, 0);
删除也是调用test_fasync,不过改了一下参数而已,实际上是通过fasync_helper函数实现的