12.1 簡介
異步通知fasync應用于系統調用signal和sigaction函數,簡單的說,signal函數就是讓一個信号與與一個函數對應,每當接收到這個信号就會調用相應的函數。
那麼什麼是異步通知?
異步通知類似于中斷的機制,當裝置可操作時,裝置驅動函數發送一個信号給核心,告知核心有資料可操作,在條件不滿足之前,并不會造成阻塞。而不像之前學的阻塞型IO和poll,它們是調用函數進去檢查,條件不滿足時還會造成阻塞。
在實際應用中,在裝置已經準備好的時候,我們希望通知使用者程式裝置已經ok,使用者程式可以讀取了,這樣應用程式就不需要一直查詢該裝置的狀态,進而節約了資源。
12.2 應用層信号捕獲
12.2.1 函數接口
檢視幫助:man signal
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); |
函數原型 | sighandler_t signal(int signum, sighandler_t handler); |
函數功能 | 将一個給定的函數和一個特定的信号關聯 |
函數參數 | signum:我們要進行處理的信号。系統的信号我們可以再終端鍵入 kill -l檢視(共64個)。其實這些信号時系統定義的宏。 handler:設定處理信号的回調函數,和中斷服務函數類似 使用範例:signal(SIGIO, mysignal); |
函數傳回值 | 函數指針 |
函數頭檔案 | #include <signal.h> |
12.2.2 信号捕獲架構示例
#include <stdio.h> #include <signal.h> /*信号處理相關的頭檔案*/ /*1.2 信号處理函數*/ void sighandler_app(int sem) { 就是捕獲到的信号值 printf("捕獲的信号: %d\n",sem); } int main(int argv,char*argc[]) { 信号綁定*/ signal(SIGINT,sighandler_app); while(1) { sleep(10); ............................................................................................................... } } |
12.2.3 信号發送示例
(1) 終端指令發送信号
指令行kill發送信号的方式:
方式1 :kill -s 信号名 程序PID号
方式2 :kill -信号名 程序PID号
鍵盤上信号發送的快捷鍵:
ctrl + \ 程式終止信号-信号代碼3
ctrl + c 程式終止信号-信号代碼2
(2) 代碼方式發送信号(system函數)
使用system函數調用終端的shell指令!
檢視幫助資訊:man system
#include <stdlib.h> int system(const char *command); |
通過代碼方式發送信号示例:
system(“kill -s 信号名 程序PID号”); |
(3) 調用kill函數發送信号
檢視幫助:
[root@xiaolong 2016-5-3]# man 1 kill [root@xiaolong 2016-5-3]# man 2 kill |
上面的數字編号就是表示檢視哪一頁的幫助資訊!
kill函數原型:
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); |
字元串轉整數函數API函數原型:
#include <stdlib.h> int atoi(const char *nptr); long atol(const char *nptr); |
C代碼方式發送信号示例:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> int main(int argc,char **argv) { int pid; int signal; if(argc!=3) { printf("傳參數格式:.app Signal信号 PID号 \n"); return -1; } 将字元串轉為整型傳回 pid=atoi(argv[2]);将字元串轉為整型傳回 發送信号 return 0; } |
12.2.4 信号說明清單
$ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX |
清單中,編号為 1 ~ 31 的信号為傳統 UNIX 支援的信号,是不可靠信号(非實時的),編号為32 ~ 63 的信号是後來擴充的,稱做可靠信号(實時信号)。不可靠信号和可靠信号的差別在于前者不支援排隊,可能會造成信号丢失,而後者不會。
下面我們對編号小于 SIGRTMIN 的信号進行讨論。
1) SIGHUP
本信号在使用者終端連接配接(正常或非正常)結束時發出, 通常是在終端的控制程序結束時, 通知同一 session 内的各個作業, 這時它們與控制終端不再關聯。
登入 Linux 時,系統會配置設定給登入使用者一個終端(Session)。在這個終端運作的所有程式,包括前台程序組和背景程序組,一般都屬于這個 Session。當使用者退出 Linux 登入時,前台程序組和背景有對終端輸出的程序将會收到 SIGHUP 信号。
這個信号的預設操作為終止程序,是以前台程序組和背景有終端輸出的程序就會中止。不過可以捕獲這個信号,比如 wget 能捕獲 SIGHUP 信号,并忽略它,這樣就算退出了 Linux 登入,wget 也 能繼續下載下傳。此外,對于與終端脫離關系的守護程序,這個信号用于通知它重新讀取配置檔案。
2) SIGINT
程式終止(interrupt)信号, 在使用者鍵入 INTR 字元(通常是 Ctrl-C)時發出,用于通知前台進
程組終止程序。
3) SIGQUIT和 SIGINT 類似, 但由 QUIT 字元(通常是 Ctrl-\)來控制. 程序在因收到 SIGQUIT 退出時會産生 core 檔案, 在這個意義上類似于一個程式錯誤信号。
4) SIGILL
執行了非法指令. 通常是因為可執行檔案本身出現錯誤, 或者試圖執行資料段. 堆棧溢出時也有可能産生這個信号。
5) SIGTRAP
由斷點指令或其它 trap 指令産生. 由 debugger 使用。
6) SIGABRT
調用 abort 函數生成的信号。
7) SIGBUS
非法位址, 包括記憶體位址對齊(alignment)出錯。比如通路一個四個字長的整數, 但其位址不是 4 的倍數。它與 SIGSEGV 的差別在于後者是由于對合法存儲位址的非法通路觸發的(如通路不屬于自己存儲空間或隻讀存儲空間)。
8) SIGFPE
在發生緻命的算術運算錯誤時發出. 不僅包括浮點運算錯誤, 還包括溢出及除數為 0 等其它所有的算術的錯誤。
9) SIGKILL
用來立即結束程式的運作. 本信号不能被阻塞、處理和忽略。如果管理者發現某個程序終止不了,可嘗試發送這個信号。
10) SIGUSR1留給使用者使用
11) SIGSEGV
試圖通路未配置設定給自己的記憶體, 或試圖往沒有寫權限的記憶體位址寫資料.
12) SIGUSR2
留給使用者使用
13) SIGPIPE
管道破裂。這個信号通常在程序間通信産生,比如采用 FIFO(管道)通信的兩個程序,讀管道沒打開或者意外終止就往管道寫,寫程序會收到 SIGPIPE 信号。此外用 Socket 通信的兩個程序,寫程序在寫 Socket 的時候,讀程序已經終止。
14) SIGALRM
時鐘定時信号, 計算的是實際的時間或時鐘時間. alarm 函數使用該信号.
15) SIGTERM
程式結束(terminate)信号, 與 SIGKILL 不同的是該信号可以被阻塞和處理。通常用來要求程式自己正常退出,shell 指令 kill 預設産生這個信号。如果程序終止不了,我們才會嘗試SIGKILL。
17) SIGCHLD
子程序結束時, 父程序會收到這個信号。如果父程序沒有處理這個信号,也沒有等待(wait)子程序,子程序雖然終止,但是還會在核心程序表中占有表項,這時的子程序稱為僵屍程序。這種情況我們應該避免(父程序或者忽略 SIGCHILD 信号,或者捕捉它,或者 wait 它派生的子程序,或者父程序先終止,這時子程序的終止自動由 init 程序來接管)。
18) SIGCONT
讓一個停止(stopped)的程序繼續執行. 本信号不能被阻塞. 可以用一個 handler 來讓程式在由 stopped 狀态變為繼續執行時完成特定的工作. 例如, 重新顯示提示符...
19) SIGSTOP
停止(stopped)程序的執行. 注意它和 terminate 以及 interrupt 的差別:該程序還未結束,隻是暫停執行. 本信号不能被阻塞, 處理或忽略.
20) SIGTSTP
停止程序的運作, 但該信号可以被處理和忽略. 使用者鍵入 SUSP 字元時(通常是 Ctrl-Z)發出這個信号
21) SIGTTIN
當背景作業要從使用者終端讀資料時, 該作業中的所有程序會收到 SIGTTIN 信号. 預設時這些程序會停止執行.
22) SIGTTOU
類似于 SIGTTIN, 但在寫終端(或修改終端模式)時收到.
23) SIGURG
有"緊急"資料或 out-of-band 資料到達 socket 時産生.
24) SIGXCPU
超過 CPU 時間資源限制. 這個限制可以由 getrlimit/setrlimit 來讀取/改變。
25) SIGXFSZ
當程序企圖擴大檔案以至于超過檔案大小資源限制。
26) SIGVTALRM
虛拟時鐘信号. 類似于 SIGALRM, 但是計算的是該程序占用的 CPU 時間.
27) SIGPROF
類似于 SIGALRM/SIGVTALRM, 但包括該程序用的 CPU 時間以及系統調用的時間.
28) SIGWINCH
視窗大小改變時發出.
29) SIGIO
檔案描述符準備就緒, 可以開始進行輸入/輸出操作.
30) SIGPWR
Power failure
31) SIGSYS
非法的系統調用。
在以上列出的信号中,程式不可捕獲、阻塞或忽略的信号有:SIGKILL,SIGSTOP,不能恢複至預設動作的信号有:SIGILL,SIGTRAP
- 預設會導緻程序流産的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
- 預設會導緻程序退出的信号有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
- 預設會導緻程序停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
- 預設程序忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH
此外,SIGIO 在 SVR4 是退出,在 4.3BSD 中是忽略;SIGCONT 在程序挂起時是繼續,否則是忽略,不能被阻塞
12.3 應用層異步通知功能設定
使用者程式必須執行 2 個步驟來使能裝置檔案的異步通知功能。 首先, 需要指定一個程序作為裝置檔案的擁有者. 當一個程序使用 fcntl 系統調用發出 F_SETOWN 指令時, 可以将這個程序PID儲存在 filp->f_owner 成員裡,留着以後使用。 這一步是讓核心知道發送信号時該通知誰。
為了真正使能異步通知, 使用者程式必須給裝置檔案設定 FASYNC 标志, 可以通過fcntl函數中的F_SETFL指令。
在這 2 個調用已被執行後, 如果驅動端有資料需要處理, 裝置檔案(驅動端)可以向程序遞交一個 SIGIO 信号,
信号發送後存儲在 filp->f_owner 成員中指定的程序(或者程序組)裡.
示例步驟:使能異步的通知功能
fp = open(argv[1],2); signal(SIGIO, mysignal); fcntl(fp,F_SETOWN,getpid()); f_flags = fcntl(fp,F_GETFL); fcntl(fp,F_SETFL,f_flags|FASYNC); |
說明:FASYNC與O_ASYNC标志都表示是異步IO标志。在不同的系統裡表示不一樣。

12.4 驅動層異步通知接口
12.4.1 驅動端操作步驟
對我們來說一個需要關注的主題是裝置驅動如何實作異步信号。
下面列出了詳細的操作順序:
當應用層發出 F_SETOWN指令時, 除了一個值被指派給 filp->f_owner,不會産生其他效果。
當應用層發出F_SETFL指令時,會設定裝置驅動的FASYNC标志,這時驅動層的 fasync 方法函數會導緻被調用。
當驅動資料需要處理時, 可以向所注冊異步通知的程序發出一個 SIGIO 信号。
驅動端需要調用的2個函數對應下面的原型:
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp); |
fasync_helper函數被用來從相關的程序清單中添加或去除入口項。
void kill_fasync(struct fasync_struct **fp, int sig, int band); |
當資料到達時可以使用kill_fasync函數發送信号來通知相關的程序。(一般在中斷處理函數裡調用)
參數:
- band參數一般都是填: POLL_IN 。表示驅動端有資料可讀!
可以填寫的選項:
#define POLL_IN (__SI_POLL|1) /* 可用的資料輸入 */
#define POLL_OUT (__SI_POLL|2) 輸出緩沖區可用 */
#define POLL_MSG (__SI_POLL|3) 輸入消息可用 */
#define POLL_ERR (__SI_POLL|4) /* i/o error */
#define POLL_PRI (__SI_POLL|5) 高優先級輸入可用 */
#define POLL_HUP (__SI_POLL|6) 裝置斷開連接配接 */
- int sig :向應用層發送的信号。(kill -l檢視)
預設情況下,裝置發送的信号時29,SIGIO信号,如果想換成其他信号,可以通過F_SETSIG指令進行。
printf("fcntl=%d\n",fcntl(fd,F_SETSIG,2)); //設定成功傳回0. |
注意: 不能設定系統預定義之外的信号。
正常情況下,程式裡加入F_SETSIG宏,編譯器時無法編譯過的。
需要加上參數: -D_GNU_SOURCE
arm-linux-gcc scanf_app.c -o app -D_GNU_SOURCE
12.4.2 驅動異步通知代碼架構
static struct fasync_struct * myfasync; //定義一個異步通知二維指針結構體 //實作檔案操作集合的函數 int xxxx_fasync (int fd, struct file * filp, int on) { /*添加異步通知的入口選項*/ fasync_helper(fd,filp,on,&myfasync); return 0; } //中斷處理函數 static void interrupu_fun(void) { 發送信号 } //release函數 static int key_release (struct inode *inode , struct file *filp) { xxxx_fasync(-1, filp, 0) ; //裝置釋放時,需要将檔案從異步通知清單中删除 return 0; } |
- 發送的信号值分析:
注意:kill_fasync函數中填的信号值沒有作用,真正發送的信号由以下結構體定義:
或者在應用層通過fcntl(fd,10,SIGBUS);函數告訴驅動應該發送什麼信号。這裡的10就是F_SETSIG宏,隻是有些編譯識别不到F_SETSIG這個宏,隻有直接寫10。SIGBUS表示将要給驅動設定的信号。設定以後,驅動裡調用kill_fasync函數将會固定發送SIGBUS信号。
12.5 分析異步通知函數的調用流程
相關路徑:\fs\fcntl.c
應用層的fcntl函數會調用驅動層的do_fcntl函數。
do_fcntl函數的原型如下:
/*系統調用函數--*/ static long do_fcntl(int fd, unsigned int cmd, unsigned long arg, struct file *filp) { long err = -EINVAL; switch (cmd) { case F_DUPFD: case F_DUPFD_CLOEXEC: if (arg >= rlimit(RLIMIT_NOFILE)) break; err = alloc_fd(arg, cmd == F_DUPFD_CLOEXEC ? O_CLOEXEC : 0); if (err >= 0) { get_file(filp); fd_install(err, filp); } break; case F_GETFD: //擷取PID号 err = get_close_on_exec(fd) ? FD_CLOEXEC : 0; break; case F_SETFD: err = 0; set_close_on_exec(fd, arg & FD_CLOEXEC); break; case F_GETFL:擷取檔案标志 err = filp->f_flags; break; case F_SETFL: err = setfl(fd, filp, arg); break; case F_GETLK: err = fcntl_getlk(filp, (struct flock __user *) arg); break; case F_SETLK: case F_SETLKW: err = fcntl_setlk(fd, filp, cmd, (struct flock __user *) arg); break; case F_GETOWN: err = f_getown(filp); force_successful_syscall_return(); break; case F_SETOWN: //設定OWN err = f_setown(filp, arg, 1); break; case F_GETOWN_EX: err = f_getown_ex(filp, arg); break; case F_SETOWN_EX: err = f_setown_ex(filp, arg); break; case F_GETSIG: err = filp->f_owner.signum; break; case F_SETSIG: /* arg == 0 restores default behaviour. */ if (!valid_signal(arg)) { break; } err = 0; filp->f_owner.signum = arg; break; case F_GETLEASE: err = fcntl_getlease(filp); break; case F_SETLEASE: err = fcntl_setlease(fd, filp, arg); break; case F_NOTIFY: err = fcntl_dirnotify(fd, filp, arg); break; case F_SETPIPE_SZ: case F_GETPIPE_SZ: err = pipe_fcntl(filp, cmd, arg); break; default: break; } return err; } |
該函數内部主要是一個switch接口,根據傳入的指令判斷主要做什麼工作!
12.6 異步通知示例代碼
下面代碼通過按鍵驅動程式示範異步IO操作方法。
12.6.1 應用層代碼示例
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <sys/epoll.h> #include <stdlib.h> #include <signal.h> int fd; /*信号處理程式*/ void sighandler(int num) { int data; read(fd,&data,4); //讀取按鍵值 printf("data=0x%X\n",data); } int main(int argc,char **argv) { int f_flags; if(argc!=2) { printf("傳參格式:./app <裝置檔案的名稱>\n"); } fd=open(argv[1],O_RDWR); if(fd<0) { printf("%s 驅動打開失敗!\n",argv[1]); return 0; } /*1. 綁定應用層将要捕獲的信号*/ signal(SIGIO,sighandler); /*2. 設定驅動檔案描述符支援異步通知功能*/ fcntl(fd,F_SETOWN,getpid()); //傳遞目前程序的PID給驅動檔案描述符 f_flags = fcntl(fd,F_GETFL); //擷取目前檔案描述符的屬性 fcntl(fd,F_SETFL,f_flags|FASYNC);//設定目前驅動支援異步IO屬性 while(1) { } close(fd); return 0; } |
12.6.2 驅動層代碼示例
#include <linux/kernel.h> //核心頭檔案 #include <linux/module.h> //子產品 #include <linux/fs.h> #include <linux/gpio.h> #include <mach/gpio.h> #include <plat/gpio-cfg.h> #include <linux/irq.h> #include <linux/interrupt.h> #include <linux/miscdevice.h> #include <linux/uaccess.h> #include <linux/wait.h> /*等待隊列頭檔案*/ #include <linux/sched.h> #include <linux/poll.h> #include <linux/signal.h> static int key_val=0; static struct fasync_struct *fapp; static int key_open(struct inode *my_inode, struct file *my_file) { printk("key_open調用成功!\n"); return 0; } static ssize_t key_read(struct file *my_file, char __user *buff, size_t cnt, loff_t *loff) { unsigned long stat=copy_to_user(buff,&key_val,4); /*向應用層傳遞資料*/ return 0; } static ssize_t key_write(struct file *my_file, const char __user *buff, size_t cnt, loff_t *loff) { printk("key_write調用成功!\n"); return 0; } static int key_release(struct inode *my_inode, struct file *my_file) { /*2. 解除安裝異步通知*/ fasync_helper(0,my_file,0,&fapp); printk("key_release調用成功!\n"); return 0; } static int key_fasync(int fd, struct file *file, int on) { /*1. 處理異步通知的信号,添加到處理清單*/ return fasync_helper(fd,file,on,&fapp); } static struct file_operations fileops= /*檔案操作接口*/ { .open=key_open, .read=key_read, .write=key_write, .release=key_release, .fasync=key_fasync, }; static struct miscdevice misc= { .minor=MISC_DYNAMIC_MINOR, /*自動配置設定次裝置号*/ .name="key_drv", .fops=&fileops, }; static struct KEY_INFO { char *irq_name; int gpio_num; int key_val; int key_irq; /*中斷号*/ }; static struct KEY_INFO key_info[4]= { {"kry_irq_1",EXYNOS4_GPX3(2),0x1}, {"kry_irq_2",EXYNOS4_GPX3(3),0x2}, {"kry_irq_3",EXYNOS4_GPX3(4),0x3}, {"kry_irq_4",EXYNOS4_GPX3(5),0x4}, }; /*按鍵的中斷服務函數*/ irqreturn_t key_irq_handler(int irq, void *dev) { struct KEY_INFO *p=(struct KEY_INFO *)dev; if(gpio_get_value(p->gpio_num)==0) { key_val=0x80|p->key_val; } else { key_val=0x80; } /*2. 向應用層發送信号*/ kill_fasync(&fapp,SIGIO,POLL_IN); return IRQ_HANDLED; } static int __init tiny4412_hello_module_init(void) { int i; for(i=0;i<4;i++) { /*擷取中斷編号*/ key_info[i].key_irq=gpio_to_irq(key_info[i].gpio_num); /*中斷的注冊函數*/ if(request_irq(key_info[i].key_irq,key_irq_handler,IRQ_TYPE_EDGE_BOTH,key_info[i].irq_name,&key_info[i])!=0) { printk("%s 中斷注冊失敗!\n",key_info[i].irq_name); } else { printk("%s 中斷注冊成功\n",key_info[i].irq_name); } } misc_register(&misc); //注冊函數 return 0; } static void __exit tiny4412_hello_module_cleanup(void) { /*中斷的登出*/ int i; for(i=0;i<4;i++) { free_irq(key_info[i].key_irq,&key_info[i]); } 中斷登出成功!\n"); misc_deregister(&misc); //登出函數 } module_init(tiny4412_hello_module_init); //驅動的入口,驅動安裝的時候調用 module_exit(tiny4412_hello_module_cleanup); //驅動的的出口,驅動解除安裝的時候調用 MODULE_LICENSE("GPL"); //驅動的許可證聲 |