Linux网络编程 | 信号 :信号函数、信号集、统一事件源 、网络编程相关信号
文章目錄
- 信號函數
- 信號集
- 統一事件源
- 網絡編程相關信號
Linux 進程信號:信號的概念、生命周期、產生流程、阻塞
在半年前我寫過一篇博客介紹了Linux中信號的概念以及處理流程,這次再來深入的講一講信號的具體使用方法以及在網絡編程中的具體應用。
信號函數
要想為一個信號設置處理函數,可以使用以下兩個系統調用。
signal
signal函數的使用非常簡單,只需要直接指定信號類型以及處理的方法即可
#include <signal.h>typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);參數
sighandler_t:函數指針,返回值為void,參數為int的函數
signum:要捕獲的信號類型
handler:指定的信號處理函數
?
返回值
調用成功:signal函數調用成功的時候會返回一個函數指針,函數指針的值為前一次調用signal函數時傳入的函數指針,或者是信號sig對應的默認處理函數指針SIG_DEF(如果時第一次調用的話)
調用失敗:signal函數調用出錯時返回SIG_ERR,并設置errno
sigaction
sigaction比起signal要更加的健壯。
#include <signal.h>int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);參數
signum:信號類型
act:指定新的信號處理方法
oact:輸出該信號之前的處理方法
?
返回值
成功:返回0
失敗:返回1并設置errno
從上面可以看到act和oldact都是sigaction結構體指針,下面來看看它里面有什么內容
struct sigaction {void (*sa_handler)(int); //信號的處理方法void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; //設置進程的信號掩碼,是一個信號集int sa_flags; //設置程序收到信號后的行為,參數如下圖void (*sa_restorer)(void); //已過時,最好不要使用 };信號集
#include<bits/sigset.h> #define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))typedef struct {unsigned long sig[_NSIG_WORDS]; } sigset_t上面是信號集的定義,從上面可以看出,其實信號集就是用于設置信號掩碼的一個位圖,他的每一個位都用來表示一個信號。
下面是信號集的基本操作
#include <signal.h> //清空信號集 int sigemptyset(sigset_t *set); //在信號集中設置所有信號 int sigfillset(sigset_t *set);//將signum信號添加進信號集中 int sigaddset(sigset_t *set, int signum);//將signum信號從信號集中刪除 int sigdelset(sigset_t *set, int signum);//查看signum信號在不在信號集中 int sigismember(const sigset_t *set, int signum);我們還可以通過以下函數來查看進程的信號掩碼
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);參數
set:設置新的信號掩碼
oldset:數據原來的信號掩碼
how:設置信號掩碼的方式
?
返回值
成功:返回0
失敗:返回-1并設置errno
當我們設置完信號掩碼之后,被屏蔽的信號就不能被進程接收,如果向進程發送一個被屏蔽的信號,則操作系統會將該信號設置為掛起信號,直到進程取消對該信號的屏蔽的時候再處理這個信號
同時,有兩個信號比較特殊,9號信號SIGKILL和19號信號SIGSTOP,這兩個信號不可被阻塞,不可被忽略,不可被自定義處理。
使用下面這個函數可以獲得當前被掛起的信號集
因為掛起信號使用信號集來進行存儲,而信號集又是一個位圖,這就意味著即使一個信號多次被接收,在位圖中也只能反應一次,并且我們結束屏蔽后對掛起信號進行處理時,也只會觸發一次信號處理函數
參數
set:用于保存被掛起的信號集
?
返回值
成功:返回0
失敗:返回-1并設置errno
統一事件源
信號是一種異步事件,所以信號處理函數和程序的主循環是兩條不同的執行路線。很顯然,信號處理函數需要盡可能快地執行完畢,確保該信號不會被屏蔽太久。(為了避免一些競態條件,信號在處理期間,系統不會再次觸發他)
一種典型的解決方案是把信號的主要處理邏輯放到程序地主循環中,當信號處理函數被觸發,它只是簡單的通知主循環程序接收到信號,并把信號值傳遞給主循環,主循環從讀取出該信號,并根據具體接收到的信號值執行對應的邏輯代碼。
信號處理函數通常都會使用管道來作為信號傳遞的媒介,那么主循環如何直到管道什么時候有數據呢?很簡單,只需要用I/O復用來監聽管道讀端的可讀事件,這樣一來信號事件就可以和其他I/O事件一樣被處理,這就是統一事件源。在Libevent庫與xinetd超級服務中也采用了這樣的機制
代碼實現如下
#include<fcntl.h> #include<signal.h> #include<stdlib.h> #include<sys/socket.h> #include<sys/epoll.h> #include<sys/types.h> #include<arpa/inet.h> #include<stdio.h> #include<errno.h> #include<netinet/in.h> #include<unistd.h>const int MAX_LISTEN = 5; const int MAX_EVENT = 1024; const int MAX_BUFFER = 1024; static int pipefd[2]; //管道描述符//設置非阻塞 int setnonblocking(int fd) {int flag = fcntl(fd, F_GETFL, 0);fcntl(fd, F_SETFL, flag |= O_NONBLOCK);return flag; }//信號處理函數 void sig_handler(int sig) {//保留原本的errno, 再函數末尾恢復, 確??芍厝胄?/span>int save_errno = errno;send(pipefd[1], (char*)&sig, 1, 0); //將信號值通過管道發送給主循環errno = save_errno; }//設置信號處理函數 void set_sig_handler(int sig) {struct sigaction sa;sa.sa_handler = sig_handler;sa.sa_flags |= SA_RESTART; //重新調用被信號中斷的系統函數sigfillset(&sa.sa_mask); //將所有信號加入信號掩碼中if(sigaction(sig, &sa, NULL) < 0){exit(EXIT_FAILURE);} }//將描述符加入epoll監聽集合中 void epoll_add_fd(int epoll_fd, int fd) {struct epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET;epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event); }int main(int argc, char*argv[]) {if(argc <= 2){printf("輸入參數:IP地址 端口號\n");return 1;}const char* ip = argv[1];int port = atoi(argv[2]);//創建監聽套接字int listen_fd = socket(PF_INET, SOCK_STREAM, 0); if(listen_fd == -1){printf("listen_fd socket.\n");return -1;}//綁定地址信息struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = inet_addr(ip);if(bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0){printf("listen_fd bind.\n");return -1;}//開始監聽if(listen(listen_fd, MAX_LISTEN) < 0){printf("listen_fd listen.\n");return -1;}//創建epoll,現版本已忽略大小,給多少都無所謂int epoll_fd = epoll_create(MAX_LISTEN);if(epoll_fd == -1){printf("epoll create.\n");return -1;}epoll_add_fd(epoll_fd, listen_fd); //將監聽套接字加入epoll中//使用sockpair創建全雙工管道,對讀端進行監控,統一事件源if(socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd) < 0){printf("socketpair.\n");return -1;}setnonblocking(pipefd[1]); //將寫端設為非阻塞epoll_add_fd(epoll_fd, pipefd[0]); //將讀端加入epoll監控集合set_sig_handler(SIGHUP); //有連接脫離終端(斷開)時發送的信號set_sig_handler(SIGCHLD); //子進程退出時發送的信號set_sig_handler(SIGINT); //接收到kill命令 set_sig_handler(SIGTERM); //用戶按下中斷鍵(DELETE或者Ctrl+C)int stop_server = 0;struct epoll_event events[MAX_LISTEN];while(!stop_server){int number = epoll_wait(epoll_fd, events, MAX_LISTEN, -1);if(number < 0 && errno != EINTR){printf("epoll_wait.\n");break;}for(int i = 0; i < number; i++){int sock_fd = events[i].data.fd;//如果監聽套接字就緒則處理連接if(sock_fd == listen_fd){struct sockaddr_in clinet_addr;socklen_t len = sizeof(clinet_addr);if(accept(listen_fd, (struct sockaddr*)&clinet_addr, &len) < 0){printf("accept.\n");continue;}epoll_add_fd(epoll_fd, sock_fd);}//如果就緒的是管道的讀端,則說明有信號到來,要處理信號else if(sock_fd == pipefd[0] && events[i].events & EPOLLIN){int sig;char signals[MAX_BUFFER];int ret = recv(pipefd[0], signals, MAX_BUFFER, 0);if(ret == -1){continue;}else if(ret == 0){continue;}else{//由于一個信號占一個字節,所以按字節逐個處理信號for(int j = 0; j < ret; j++){switch (signals[i]){//這兩個信號主要是某個連接或者子進程退出,對主流程影響不大,直接忽略case SIGCHLD:case SIGHUP:{continue;}//這兩個信號主要是強制中斷主流程,所以結束服務(不能直接退出,因為還有描述符未關閉)case SIGTERM:case SIGINT:{stop_server = 1;}}}}}//處理讀就緒與寫就緒,因為這里主要演示統一事件源,所以不實現這塊的邏輯else{/* */}}}//關閉文件描述符close(listen_fd);close(pipefd[1]);close(pipefd[0]);return 0; }網絡編程相關信號
SIGHUP
當某個進程脫離終端時(正?;蛘叻钦?#xff09;,SIGHUP信號都將被觸發。該信號的默認處理方式是終止收到該信號的進程,因此如果沒有捕捉該信號,則進程就會退出。在網絡編程中通常使用SIGHUP信號來強制服務器重讀配置文件。
SIGPIPE
SIGPIPE是往一個讀端關閉的描述符寫數據時通知的信號,例如我們往一個讀端關閉的管道或者socket連接中寫數據就會引發SIGPIPE信號,并且將errno設置為EPIPE。
由于程序接收到SIGPIPE信號就會默認結束進程,所以我們需要在代碼中捕獲并且處理該信號,或至少忽略它,因為我們通常不會希望因為錯誤的寫操作而導致程序退出
SIGURG
在Linux環境下,內核通知應用程序帶外數據到達主要有兩種方法,一種是I/O復用如select等系統調用在接收到帶外數據的時候就會返回,并且會向應用程序報告socket上的異常事件。另一種方法就是使用SIGURG信號。
總結
以上是生活随笔為你收集整理的Linux网络编程 | 信号 :信号函数、信号集、统一事件源 、网络编程相关信号的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux网络编程 | 并发模式:半同步
- 下一篇: Linux网络编程 | 定时事件 :Li