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