定时器 槽函数没执行_Web服务器项目详解 07 定时器处理非活动连接(上)
點擊“兩猿社”
?關注我們
?Web服務器詳解目錄
00 項目概述
01 線程同步機制包裝類
02 半同步/半反應堆線程池(上)
03 半同步/半反應堆線程池(下)
04 http連接處理(上)
05 http連接處理(中)
06 http連接處理(下)
07 定時器處理非活動連接(上)
08 定時器處理非活動連接(下)
09 日志系統(上)
10 日志系統(下)
11 數據連接池
12 注冊和登錄校驗
13 服務器測試
14 項目遇到的問題及解決方案
15 項目涉及的常見面試題
基礎知識
非活躍,是指客戶端(這里是瀏覽器)與服務器端建立連接后,長時間不交換數據,一直占用服務器端的文件描述符,導致連接資源的浪費。
定時事件,是指固定一段時間之后觸發某段代碼,由該段代碼處理一個事件,如定期檢測非活躍連接。
定時器,是指利用結構體或其他形式,將多種定時事件進行封裝起來。具體的,這里只涉及一種定時事件,即定期檢測非活躍連接,這里將該定時事件與連接資源封裝為一個結構體定時器。
定時器容器,是指使用某種容器類數據結構,將上述多個定時器組合起來,便于對定時事件統一管理。具體的,項目中使用升序鏈表將所有定時器串聯組織起來。
整體概述
本項目中,服務器主循環為每一個連接創建一個定時器,并對每個連接進行定時。另外,利用升序時間鏈表容器將所有定時器串聯起來,若主循環接收到定時通知,則在鏈表中依次執行定時任務。
Linux下提供了三種定時的方法:
socket選項SO_RECVTIMEO和SO_SNDTIMEO
SIGALRM信號
I/O復用系統調用的超時參數
三種方法沒有一勞永逸的應用場景,也沒有絕對的優劣。由于項目中使用的是SIGALRM信號,這里僅對其進行介紹,另外兩種方法可以查閱游雙的Linux高性能服務器編程 第11章 定時器。
具體的,利用alarm函數周期性地觸發SIGALRM信號,信號處理函數利用管道通知主循環,主循環接收到該信號后對升序鏈表上所有定時器進行處理,若該段時間內沒有交換數據,則將該連接關閉,釋放所占用的資源。
從上面的簡要描述中,可以看出定時器處理非活動連接模塊,主要分為兩部分,其一為定時方法與信號通知流程,其二為定時器及其容器設計與定時任務的處理。
本文內容
本篇將介紹定時方法與信號通知流程,具體的涉及到基礎API、信號通知流程和代碼實現。
基礎API,描述sigaction結構體、sigaction函數、sigfillset函數、SIGALRM信號、SIGTERM信號、alarm函數、socketpair函數、send函數。
信號通知流程,介紹統一事件源和信號處理機制。
代碼實現,結合代碼對信號處理函數的設計與使用進行詳解。
基礎API
為了更好的源碼閱讀體驗,這里提前對代碼中使用的一些API進行簡要介紹,更豐富的用法可以自行查閱資料。
sigaction結構體
1struct?sigaction?{2????void?(*sa_handler)(int);
3????void?(*sa_sigaction)(int,?siginfo_t?*,?void?*);
4????sigset_t?sa_mask;
5????int?sa_flags;
6????void?(*sa_restorer)(void);
7}
sa_handler是一個函數指針,指向信號處理函數
sa_sigaction同樣是信號處理函數,有三個參數,可以獲得關于信號更詳細的信息
sa_mask用來指定在信號處理函數執行期間需要被屏蔽的信號
sa_flags用于指定信號處理的行為
SA_RESTART,使被信號打斷的系統調用自動重新發起
SA_NOCLDSTOP,使父進程在它的子進程暫停或繼續運行時不會收到 SIGCHLD 信號
SA_NOCLDWAIT,使父進程在它的子進程退出時不會收到 SIGCHLD 信號,這時子進程如果退出也不會成為僵尸進程
SA_NODEFER,使對信號的屏蔽無效,即在信號處理函數執行期間仍能發出這個信號
SA_RESETHAND,信號處理之后重新設置為默認的處理方式
SA_SIGINFO,使用 sa_sigaction 成員而不是 sa_handler 作為信號處理函數
sa_restorer一般不使用
sigaction函數
1#include?2
3int?sigaction(int?signum,?const?struct?sigaction?*act,?struct?sigaction?*oldact);
signum表示操作的信號。
act表示對信號設置新的處理方式。
oldact表示信號原來的處理方式。
返回值,0 表示成功,-1 表示有錯誤發生。
sigfillset函數
1#include?2
3int?sigfillset(sigset_t?*set);
用來將參數set信號集初始化,然后把所有的信號加入到此信號集里。
SIGALRM、SIGTERM信號
1#define?SIGALRM??14?????//由alarm系統調用產生timer時鐘信號2#define?SIGTERM??15?????//終端發送的終止信號
alarm函數
1#include?;2
3unsigned?int?alarm(unsigned?int?seconds);
設置信號傳送鬧鐘,即用來設置信號SIGALRM在經過參數seconds秒數后發送給目前的進程。如果未設置信號SIGALRM的處理函數,那么alarm()默認處理終止進程.
socketpair函數
在linux下,使用socketpair函數能夠創建一對套接字進行通信,項目中使用管道通信。
1#include?2#include?
3
4int?socketpair(int?domain,?int?type,?int?protocol,?int?sv[2]);
domain表示協議族,PF_UNIX或者AF_UNIX
type表示協議,可以是SOCK_STREAM或者SOCK_DGRAM,SOCK_STREAM基于TCP,SOCK_DGRAM基于UDP
protocol表示類型,只能為0
sv[2]表示套節字柄對,該兩個句柄作用相同,均能進行讀寫雙向操作
返回結果, 0為創建成功,-1為創建失敗
send函數
1#include?2#include?
3
4ssize_t?send(int?sockfd,?const?void?*buf,?size_t?len,?int?flags);
當套接字發送緩沖區變滿時,send通常會阻塞,除非套接字設置為非阻塞模式,當緩沖區變滿時,返回EAGAIN或者EWOULDBLOCK錯誤,此時可以調用select函數來監視何時可以發送數據。
信號通知流程
Linux下的信號采用的異步處理機制,信號處理函數和當前進程是兩條不同的執行路線。具體的,當進程收到信號時,操作系統會中斷進程當前的正常流程,轉而進入信號處理函數執行操作,完成后再返回中斷的地方繼續執行。
為避免信號競態現象發生,信號處理期間系統不會再次觸發它。所以,為確保該信號不被屏蔽太久,信號處理函數需要盡可能快地執行完畢。
一般的信號處理函數需要處理該信號對應的邏輯,當該邏輯比較復雜時,信號處理函數執行時間過長,會導致信號屏蔽太久。
這里的解決方案是,信號處理函數僅僅發送信號通知程序主循環,將信號對應的處理邏輯放在程序主循環中,由主循環執行信號對應的邏輯代碼。
統一事件源
統一事件源,是指將信號事件與其他事件一樣被處理。
具體的,信號處理函數使用管道將信號傳遞給主循環,信號處理函數往管道的寫端寫入信號值,主循環則從管道的讀端讀出信號值,使用I/O復用系統調用來監聽管道讀端的可讀事件,這樣信號事件與其他文件描述符都可以通過epoll來監測,從而實現統一處理。
信號處理機制
每個進程之中,都有存著一個表,里面存著每種信號所代表的含義,內核通過設置表項中每一個位來標識對應的信號類型。
信號的接收
接收信號的任務是由內核代理的,當內核接收到信號后,會將其放到對應進程的信號隊列中,同時向進程發送一個中斷,使其陷入內核態。注意,此時信號還只是在隊列中,對進程來說暫時是不知道有信號到來的。
信號的檢測
進程從內核態返回到用戶態前進行信號檢測
進程在內核態中,從睡眠狀態被喚醒的時候進行信號檢測
進程陷入內核態后,有兩種場景會對信號進行檢測:
當發現有新信號時,便會進入下一步,信號的處理。
信號的處理
( 內核 )信號處理函數是運行在用戶態的,調用處理函數前,內核會將當前內核棧的內容備份拷貝到用戶棧上,并且修改指令寄存器(eip)將其指向信號處理函數。
( 用戶 )接下來進程返回到用戶態中,執行相應的信號處理函數。
( 內核 )信號處理函數執行完成后,還需要返回內核態,檢查是否還有其它信號未處理。
( 用戶 )如果所有信號都處理完成,就會將內核棧恢復(從用戶棧的備份拷貝回來),同時恢復指令寄存器(eip)將其指向中斷前的運行位置,最后回到用戶態繼續執行進程。
至此,一個完整的信號處理流程便結束了,如果同時有多個信號到達,上面的處理流程會在第2步和第3步驟間重復進行。
代碼分析
信號處理函數
自定義信號處理函數,創建sigaction結構體變量,設置信號函數。
1//信號處理函數2void?sig_handler(int?sig) 3{
4????//為保證函數的可重入性,保留原來的errno
5????//可重入性表示中斷后再次進入該函數,環境變量與之前相同,不會丟失數據
6????int?save_errno?=?errno;
7????int?msg?=?sig;
8
9????//將信號值從管道寫端寫入,傳輸字符類型,而非整型
10????send(pipefd[1],?(char?*)&msg,?1,?0);
11
12????//將原來的errno賦值為當前的errno
13????errno?=?save_errno;
14}
信號處理函數中僅僅通過管道發送信號值,不處理信號對應的邏輯,縮短異步執行時間,減少對主程序的影響。
1//設置信號函數2void?addsig(int?sig,?void(handler)(int),?bool?restart?=?true)
3{
4????//創建sigaction結構體變量
5????struct?sigaction?sa;
6????memset(&sa,?'\0',?sizeof(sa));
7
8????//信號處理函數中僅僅發送信號值,不做對應邏輯處理
9????sa.sa_handler?=?handler;
10????if?(restart)
11????????sa.sa_flags?|=?SA_RESTART;
12????//將所有信號添加到信號集中
13????sigfillset(&sa.sa_mask);
14
15????//執行sigaction函數
16????assert(sigaction(sig,?&sa,?NULL)?!=?-1);
17}
項目中設置信號函數,僅關注SIGTERM和SIGALRM兩個信號。
信號通知邏輯
創建管道,其中管道寫端寫入信號值,管道讀端通過I/O復用系統監測讀事件
設置信號處理函數SIGALRM(時間到了觸發)和SIGTERM(kill會觸發,Ctrl+C)
通過struct sigaction結構體和sigaction函數注冊信號捕捉函數
在結構體的handler參數設置信號處理函數,具體的,從管道寫端寫入信號的名字
利用I/O復用系統監聽管道讀端文件描述符的可讀事件
信息值傳遞給主循環,主循環再根據接收到的信號值執行目標信號對應的邏輯代碼
代碼分析
1//創建管道套接字2ret?=?socketpair(PF_UNIX,?SOCK_STREAM,?0,?pipefd);
3assert(ret?!=?-1);
4
5//設置管道寫端為非阻塞,為什么寫端要非阻塞?
6setnonblocking(pipefd[1]);
7
8//設置管道讀端為ET非阻塞
9addfd(epollfd,?pipefd[0],?false);
10
11//傳遞給主循環的信號值,這里只關注SIGALRM和SIGTERM
12addsig(SIGALRM,?sig_handler,?false);
13addsig(SIGTERM,?sig_handler,?false);
14
15//循環條件
16bool?stop_server?=?false;
17
18//超時標志
19bool?timeout?=?false;
20
21//每隔TIMESLOT時間觸發SIGALRM信號
22alarm(TIMESLOT);
23
24while?(!stop_server)
25{
26????//監測發生事件的文件描述符
27????int?number?=?epoll_wait(epollfd,?events,?MAX_EVENT_NUMBER,?-1);
28????if?(number?0?&&?errno?!=?EINTR)
29????{
30????????break;
31????}
32
33????//輪詢文件描述符
34????for?(int?i?=?0;?i?35????{
36????????int?sockfd?=?events[i].data.fd;
37
38????????//管道讀端對應文件描述符發生讀事件
39????????if?((sockfd?==?pipefd[0])?&&?(events[i].events?&?EPOLLIN))
40????????{
41????????????int?sig;
42????????????char?signals[1024];
43
44????????????//從管道讀端讀出信號值,成功返回字節數,失敗返回-1
45????????????//正常情況下,這里的ret返回值總是1,只有14和15兩個ASCII碼對應的字符
46????????????ret?=?recv(pipefd[0],?signals,?sizeof(signals),?0);
47????????????if?(ret?==?-1)
48????????????{
49????????????????//?handle?the?error
50????????????????continue;
51????????????}
52????????????else?if?(ret?==?0)
53????????????{
54????????????????continue;
55????????????}
56????????????else
57????????????{
58????????????????//處理信號值對應的邏輯
59????????????????for?(int?i?=?0;?i?60????????????????{
61????????????????????//這里面明明是字符
62????????????????????switch?(signals[i])
63????????????????????{
64????????????????????//這里是整型
65????????????????????case?SIGALRM:
66????????????????????{
67????????????????????????timeout?=?true;
68????????????????????????break;
69????????????????????}
70????????????????????case?SIGTERM:
71????????????????????{
72????????????????????????stop_server?=?true;
73????????????????????}
74????????????????????}
75????????????????}
76????????????}
77????????}
78????}
79}
為什么管道寫端要非阻塞?
send是將信息發送給套接字緩沖區,如果緩沖區滿了,則會阻塞,這時候會進一步增加信號處理函數的執行時間,為此,將其修改為非阻塞。
沒有對非阻塞返回值處理,如果緩沖區滿了是不是意味著這一次定時事件失效了?
是的,但定時事件是非必須立即處理的事件,可以允許這樣的情況發生。
管道傳遞的是什么類型?switch-case的變量沖突?
信號本身是整型數值,管道中傳遞的是ASCII碼表中整型數值對應的字符。switch的變量一般為字符或整型,當為字符時,case中可以是字符,也可以是字符對應的ASCII碼。
如果本文對你有幫助,閱讀原文star一下服務器項目,我們需要你的星星。
完。
?長
按
關
注
兩猿社
微信號 : twomonkeysclub
懂點互聯網,懂點IC的程序猿。
帶你豐富項目經驗,輕松校招。
我知道你在看喲
總結
以上是生活随笔為你收集整理的定时器 槽函数没执行_Web服务器项目详解 07 定时器处理非活动连接(上)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 三星电子聘用前高通公司副总裁,发力汽车芯
- 下一篇: 人民日报评视频会员收费:挖掘自身而不是向