Linux fd 系列|信號(hào)編程(signal)竟能這樣做?漲姿勢(shì)了!

信號(hào)是什么?
Linux 操作系統(tǒng)
首先說(shuō),信號(hào)(signal)是什么?
信號(hào)( signal )本質(zhì)是 Linux 進(jìn)程間通信的一種機(jī)制,也叫軟中斷信號(hào)。既然是通信機(jī)制,那么就是傳遞信息用的,信號(hào)傳遞的信息很簡(jiǎn)單,就是一個(gè)整數(shù),一般用于配合系統(tǒng)管理任務(wù),比如進(jìn)程的終結(jié)、恢復(fù)、熱加載等。
信號(hào)都用整數(shù)常量表示,命名以 SIG 為前綴,比如 SIGINT( ctrl-c 觸發(fā)),SIGKILL( kill -9 觸發(fā) )。
信號(hào)一般怎么產(chǎn)生?
由內(nèi)核產(chǎn)生,比如內(nèi)存錯(cuò)誤,除 0 等錯(cuò)誤,內(nèi)核通過(guò)信號(hào)通知到相應(yīng)的進(jìn)程;
可以由其他進(jìn)程傳遞給目標(biāo)進(jìn)程,比如 kill 命令就是專(zhuān)門(mén)干這個(gè)事情的;
信號(hào)處理分為兩個(gè)階段:
發(fā)送階段:內(nèi)核將信號(hào)(signal)放到對(duì)應(yīng)的 pending 隊(duì)列中;
傳遞階段:也叫做處理階段,內(nèi)核將信號(hào)從 pending 隊(duì)列中取出來(lái),并且進(jìn)行處理,一般是調(diào)用相應(yīng)的回調(diào)函數(shù)(處理方式有三種:用戶(hù)定義、內(nèi)核默認(rèn)定義 SIG_DEL、忽略 SIG_IGN);
signalfd 是什么?
了解了什么是信號(hào)( signal ),那 signalfd 又會(huì)是什么呢?
是一個(gè)跟信號(hào)關(guān)聯(lián)的文件描述符,能夠以 io 的行為獲取到系統(tǒng)信號(hào),屬性上來(lái)講 signalfd 也是一個(gè)匿名 fd 類(lèi)型。
signalfd 長(zhǎng)什么樣子?
奇伢按照 man signalfd 里面的例子,寫(xiě)了個(gè) demo,跑在 Linux 機(jī)器上,按照慣例去看下 fd 的樣子。
從這里可以得到簡(jiǎn)單的信息:
signal 用的匿名 inode ,signalfd 屬于匿名 fd 的一種;
句柄關(guān)聯(lián)的重要信息就是 sigmask,通過(guò) /proc/${pid}/fdinfo/3 能看到這個(gè)值;
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個(gè)人覺(jué)得比較好的學(xué)習(xí)書(shū)籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。?!前100名進(jìn)群領(lǐng)取,額外贈(zèng)送一份價(jià)值699的內(nèi)核資料包(含視頻教程、電子書(shū)、實(shí)戰(zhàn)項(xiàng)目及代碼)?

signalfd 使用姿勢(shì)?
其實(shí)信號(hào)是很講究的,甚至有信號(hào)編程一說(shuō),Linux 的 signalfd 為信號(hào)的處理提供了一種新的方法,統(tǒng)一到文件的 io 模式,契合一切接文件的理念。
系統(tǒng)調(diào)用:
該系統(tǒng)調(diào)用返回一個(gè)整數(shù)類(lèi)型 signalfd,這個(gè)句柄跟信號(hào)行為綁定,當(dāng)發(fā)生信號(hào)的時(shí)候,句柄觸發(fā)可讀事件。
第一個(gè)參數(shù)也可以傳入一個(gè)有效的信號(hào) fd 的句柄,如果傳入的是 -1 ,那么內(nèi)核會(huì)自動(dòng)創(chuàng)建一個(gè)新的 fd 。
完整的代碼例子,在 Linux 機(jī)器上,通過(guò) man signalfd 就可以獲取到。
上面的例子,signalfd 沒(méi)有信號(hào)(沒(méi)有可讀事件)的時(shí)候會(huì)阻塞在read調(diào)用上,運(yùn)行效果如下:
可以看到每一次 ctrl + c 觸發(fā)的信號(hào)被捕捉到,并且打印出來(lái)。用文件 io 的方式來(lái)接收信號(hào),牛。
怎么做到的呢?照例,我們淺析一下內(nèi)核的代碼,位于 fs/signalfd.c,這是一個(gè)很小的文件,正是這個(gè)文件完成了對(duì)信號(hào)“文件化”的封裝。
上面最重要的兩個(gè)調(diào)用:
sigprocmask :設(shè)置當(dāng)前進(jìn)程的信號(hào)掩碼,把 SIGINT,SIGQUIT 處理屏蔽掉,關(guān)閉內(nèi)核默認(rèn)行為;
signalfd :獲取到一個(gè)和信號(hào)關(guān)聯(lián)的“文件”句柄;
signalfd 原理剖析
環(huán)境聲明:
Linux 內(nèi)核版本 4.19
1、signalfd
看一下 signalfd 支持的接口調(diào)用:
通過(guò)這個(gè)可以知道 signalfd 支持的特性:
支持 /proc/${pid}/fdinfo/xx 查看信息( 對(duì)應(yīng) signalfd_show_fdinfo 函數(shù) );
支持 read,close 調(diào)用 ( 對(duì)應(yīng) signalfd_read 函數(shù) );
支持 poll 調(diào)用,支持 epoll 管理( 對(duì)應(yīng) signalfd_poll 函數(shù) );
2、signalfd_poll
這個(gè)函數(shù)做的事情非常簡(jiǎn)單,就是把等待對(duì)象掛到當(dāng)前進(jìn)程的信號(hào)結(jié)構(gòu)的鏈表上。表頭是:current->sighand->signalfd_wqh ,這個(gè)就有意思了,這里直接掛到當(dāng)前進(jìn)程的結(jié)構(gòu)上。換句話(huà)說(shuō),喚醒也是自此表頭開(kāi)始。
回憶一下 timerfd ,是掛在 timerfd_ctx->wqh 的字段上。這里的差別是因?yàn)樾盘?hào)是對(duì)進(jìn)程來(lái)說(shuō)的。
3、signalfd_read
讀一個(gè) signalfd 的操作非常簡(jiǎn)單,主要邏輯:
查看當(dāng)前隊(duì)列中是否有信號(hào),有的話(huà)就取出來(lái),填充到用戶(hù)給的結(jié)構(gòu)體中;
如果句柄是阻塞類(lèi)型的,在沒(méi)有信號(hào)的時(shí)候,會(huì)切走 cpu,等到有信號(hào)的時(shí)候切回來(lái)。如果是非阻塞類(lèi)型的,直接報(bào)錯(cuò),返回 EAGAIN ;
簡(jiǎn)要的代碼注釋如下:
這里就能非常清晰的看到,進(jìn)程有信號(hào)的時(shí)候,signalfd 句柄就是可讀的。
signal 和 epoll 的配合
1、熟悉的 epoll_ctl
epoll_ctl 注冊(cè) signalfd 的時(shí)候,調(diào)用 signalfd_poll ,signalfd_poll 會(huì)把 epoll 創(chuàng)建的 wait entry 掛到 current->sighand 上。喚醒的時(shí)候調(diào)用這個(gè) wait 鏈表的回調(diào)。
2、什么時(shí)候喚醒呢?
喚醒的操作其實(shí)不在 signalfd.c 文件中,而是在原有的信號(hào)軟中斷的流程中。
在內(nèi)核函數(shù) signalfd_notify ?中,會(huì)判斷進(jìn)程的 sighand->signalfd_wqh 是否非空,如果非空,說(shuō)明有人關(guān)注這個(gè)信號(hào),那么就會(huì)通知到對(duì)應(yīng)的 waiter 。
為了知識(shí)的完整性,說(shuō)個(gè)點(diǎn),signalfd_notify 其實(shí)在 timer 定時(shí)器的流程中也有調(diào)用,但跟我們本次主干沒(méi)啥關(guān)系,這里忽略。
信號(hào)的發(fā)送喚醒的簡(jiǎn)要示意圖:

所有的信號(hào)發(fā)送都會(huì)調(diào)用到send_signal,在這個(gè)里面實(shí)現(xiàn)了喚醒sighand->signalfd_wqh鏈表的操作。從而使得 epoll 感知到 signalfd 可讀了(因?yàn)閬?lái)信號(hào)了),使得 epoll 從 epoll_wait 出喚醒,然后調(diào)用 read 操作,把信號(hào)的相關(guān)信息從句柄中讀出來(lái)。
劃重點(diǎn):?jiǎn)拘言谛盘?hào)發(fā)送的過(guò)程。
總結(jié)
信號(hào)能夠像文件一樣 read 出來(lái),這種優(yōu)雅的信號(hào)處理方式得益于 signalfd 的封裝;
信號(hào)是掛在在進(jìn)程 task_struct 結(jié)構(gòu)體上的,信號(hào)隊(duì)列非空的時(shí)候 signalfd 句柄可讀;
和 epoll 池的配合同樣還是老套路,epoll_ctl 注冊(cè)的時(shí)候調(diào)用 .poll ?接口掛載 epoll 的 wait entry 到 sighand->signalfd_wqh 之上,信號(hào)發(fā)送時(shí)調(diào)用 signalfd_notify ?喚醒 epoll ;
signalfd 也是一種匿名 fd 類(lèi)型;
