日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

Linux网络编程 | 定时事件 :Linux常见定时方法、定时器链表、空闲断开

發布時間:2024/4/11 linux 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Linux网络编程 | 定时事件 :Linux常见定时方法、定时器链表、空闲断开 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

  • Linux定時方法
  • 定時器鏈表
  • 空閑斷開


Linux定時方法

Linux中為我們提供了三種定時方法,分別是Socket超時選項,SIGALRM信號,I/O復用超時參數。下面一一對其進行介紹。

Socket超時選項
socket中的SO_RCVTIMEOSO_SNDTIMEO選項分別用來設置接收數據超時時間發送數據超時時間 。所以這兩個選項僅僅適用于那些用來收發數據的socket系統調用,如send,recv,recvmsg,accept,connect。

下面是這兩個選項對這些系統調用的影響

I/O復用超時參數
在Linux下的三組I/O復用系統調用都帶有超時參數,所以他們不僅可以統一處理信號和I/O時間,也能統一處理定時事件。
但是I/O復用系統調用可能會在超時時間到期之前提前返回(有I/O事件就緒),所以如果要使用該參數進行定時,就需要不斷更新定時參數來反應剩余的時間。

SIGALRM信號
Linux下的alarm函數和setitimer函數也可以用于設定鬧鐘,一旦鬧鐘到時,就會觸發SIGALRM信號 ,所以我們可以利用該信號的處理函數來處理定時任務。

#include <unistd.h> unsigned int alarm(unsigned int seconds);#include <sys/time.h> int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);struct itimerval {struct timeval it_interval; /* next value */struct timeval it_value; /* current value */ };struct timeval {time_t tv_sec; /* seconds */suseconds_t tv_usec; /* microseconds */ };

定時器鏈表

由于服務器程序通常管理著眾多定時事件,因此有效地組織這些定時事件,使他們能夠在預期的時間點被觸發并且不影響服務器的主要邏輯,對于服務器的性能有著至關重要的影響。因此我們通常會將每個定時事件封裝成定時器,并利用某種容器類數據結構對定時事件進行統一管理。

下面就使用一個以到期時間進行升序排序的雙向帶頭尾節點的鏈表來作為容器,實現定時器鏈表。
具體的細節以及實現思路都寫在了注釋里。

#ifndef __TIMER_LIST_H__ #define __TIMER_LIST_H__#include<time.h> #include<stdio.h> #include<sys/socket.h> #include<sys/types.h> #include<netinet/in.h>const int MAX_BUFFER_SIZE = 1024;class util_timer;//用戶數據 struct client_data {sockaddr_in addr;int sock_fd;char buff[MAX_BUFFER_SIZE];util_timer* timer; };//定時器類 struct util_timer {public:util_timer(): _next(nullptr), _prev(nullptr){}time_t _expire; //到期時間void (*fun)(client_data*); //處理函數client_data* _user_data; //用戶參數util_timer* _next;util_timer* _prev; };//定時器鏈表,帶頭尾雙向鏈表,定時器以升序排序 class timer_list {typedef util_timer node;public:timer_list(): _head(nullptr), _tail(nullptr){}~timer_list(){node* cur = _head;while(cur){node* next = cur->_next;delete cur;cur = next;}}//插入定時器void push(node* timer){if(timer == nullptr){return;}//如果頭節點為空,則讓新節點成為頭節點if(_head == nullptr){_head = _tail = timer;return;}//如果節點比頭節點小,則讓他成為新的節點if(timer->_expire < _head->_expire){timer->_next = _head;_head->_prev = timer; _head = timer;return;}node* prev = _head;node* cur = _head->_next;//找到插入的位置while(cur){if(timer->_expire < cur->_expire){timer->_next = cur;cur->_prev = timer;prev->_next = timer;timer->_prev = prev;return;}prev = cur;cur = cur->_next;}//如果走到這里還沒有返回,則說明當前定時器大于鏈表中所有節點,所以讓他成為新的尾節點if(cur == nullptr){prev->_next = timer;timer->_prev = prev;timer->_next = nullptr;_tail = timer;}}//如果節點的時間發生修改,則將他調整到合適的位置上void adjust_node(node* timer){if(timer == nullptr){return;}//先將節點從鏈表中取出,再插回去。if(timer == _head && timer == _tail){_head = _tail = nullptr;}//如果該節點是頭節點if(timer == _head){_head = timer->_next;if(_head){_head->_prev = nullptr;}}//如果該節點是尾節點if(timer == _tail){_tail = _tail->_prev;if(_tail){_tail->_next = nullptr;}}//該節點在中間else{timer->_prev->_next = timer->_next;timer->_next->_prev = timer->_prev;}//將節點重新插入回鏈表中push(timer);}//刪除指定定時器void pop(node* timer){if(timer == nullptr){return;}//如果鏈表中只有一個節點if(timer == _head && timer == _tail){delete timer;_head = _tail = nullptr;}//如果刪除的是頭節點else if(timer == _head){_head = _head->_next;_head->_prev = nullptr;delete timer;timer = nullptr;}//如果刪除的是尾節點else if(timer == _tail){_tail = _tail->_prev;_tail->_next = nullptr;delete timer;timer = nullptr;}else{//此時刪除節點就是中間的節點timer->_prev->_next = timer->_next;timer->_next->_prev = timer->_prev;delete timer;timer = nullptr;}}//處理鏈表上的到期任務void tick(){//此時鏈表中沒有節點if(_head == nullptr){return;}printf("time tick\n");time_t cur_time = time(nullptr); //獲取當前時間node* cur = _head;while(cur){//由于鏈表是按照到期時間進行排序的,所以如果當前節點沒到期,后面的也不可能到期if(cur->_expire > cur_time){break;}//如果當前節點到期,則調用回調函數執行定時任務。cur->fun(cur->_user_data);//執行完定時任務后,將節點從鏈表中刪除node* next = cur->_next;//前指針置空if(next != nullptr){next->_prev = nullptr;}delete cur;cur = next;}}private:node* _head;node* _tail; }; #endif // !__TIMER_LIST_H__

時間復雜度
添加節點:O(n)
刪除節點:O(1)
執行定時任務:O(1)


空閑斷開

對于服務器來說,定期處理非活動連接是保證其高可用的一項不必可少的功能,下面就以上一篇博客中實現的統一事件源服務器舉例,演示一下如何使用定時器鏈表來實現空閑斷開的功能。
Linux網絡編程 | 信號 :信號函數、信號集、統一事件源 、網絡編程相關信號

實現的思路很簡單,我們為每一個連接設定一個定時器,將定時器放入定時器鏈表中,并通過alarm函數來周期性觸發SIGALRM信號。
如果某一個連接當前有新的活動,則說明該連接為活躍連接,重置其定時器并且調整定時器在鏈表中的位置。
我們設定一個監控周期,每當周期到則會觸發SIGALRM信號,信號處理函數則利用管道將信號發送給主循環。如果主循環監控的管道讀端有數據,并且待處理信號為SIGALRM,則說明此時需要執行定時器鏈表上的定時任務(關閉當前不活躍的連接)。

#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>#include"timer_list.h" const int MAX_LISTEN = 5; const int MAX_EVENT = 1024; const int MAX_BUFFER = 1024; const int TIMESLOT = 5; const int FD_LIMIT = 65535;static int pipefd[2]; //管道描述符 static int epoll_fd = 0; //epoll操作句柄 static timer_list timer_lst; //定時器鏈表 //設置非阻塞 int setnonblocking(int fd) {int flag = fcntl(fd, F_GETFL, 0);fcntl(fd, F_SETFL, flag |= O_NONBLOCK);return flag; }//將描述符加入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); }//信號處理函數 void sig_handler(int sig) {//保留原本的errno, 再函數末尾恢復, 確保可重入性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);} }//alarm信號處理函數 void timer_handler() {timer_lst.tick(); //執行到期任務alarm(TIMESLOT); //開始下一輪計時 }//到時處理任務 void handler(client_data* user_data) {if(user_data == nullptr){return;}//將過期連接的從epoll中移除,并關閉描述符epoll_ctl(epoll_fd, EPOLL_CTL_DEL, user_data->sock_fd, NULL);close(user_data->sock_fd);printf("close fd : %d\n", user_data->sock_fd); }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(SIGALRM); //設置定時信號set_sig_handler(SIGTERM); //用戶按下中斷鍵(DELETE或者Ctrl+C)struct epoll_event events[MAX_LISTEN];client_data* users = new client_data[FD_LIMIT];bool stop_server = false;bool time_out;alarm(TIMESLOT); //開始計時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);int conn_fd = accept(listen_fd, (struct sockaddr*)&clinet_addr, &len);if(conn_fd < 0){printf("accept.\n");continue;}epoll_add_fd(epoll_fd, sock_fd);//存儲用戶信息users[conn_fd].addr = clinet_addr;users[conn_fd].sock_fd = conn_fd;//創建定時器util_timer* timer = new util_timer;users->timer = timer;timer->_user_data = &users[conn_fd];timer->fun = handler;time_t cur_time = time(nullptr);timer->_expire = cur_time + 3 * TIMESLOT; //設置超時時間timer_lst.push(timer); //將定時器放入定時器鏈表中}//如果就緒的是管道的讀端,則說明有信號到來,要處理信號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 SIGALRM:{time_out = true;break;}case SIGINT:{stop_server = true;}}}}}//如果就緒的是可讀事件else if(events[i].events & EPOLLIN){int ret = recv(sock_fd, users[sock_fd].buff, MAX_BUFFER - 1, 0); util_timer* timer = users[sock_fd].timer;//連接出現問題,斷開連接并且刪除對應定時器if(ret < 0){if(errno != EAGAIN){handler(&users[sock_fd]);if(timer){timer_lst.pop(timer);}}}//如果讀寫出現問題,也斷開連接else if(ret == 0){handler(&users[sock_fd]);if(timer){timer_lst.pop(timer);}}else{//如果事件成功執行,則重新設置定時器的超時時間,并調整其在定時器鏈表中的位置if(timer){time_t cur_time = time(nullptr);timer->_expire = cur_time + 3 * TIMESLOT;timer_lst.adjust_node(timer);}}}else{/* 業務邏輯和寫事件暫不實現,本程序主要用于定時器鏈表如何實現空閑斷開功能 */}}//如果超時,則調用超時處理函數,并且重置標記 if(time_out == true){timer_handler();time_out = false;}}//關閉文件描述符close(listen_fd);close(pipefd[1]);close(pipefd[0]);delete[] users;return 0; }

總結

以上是生活随笔為你收集整理的Linux网络编程 | 定时事件 :Linux常见定时方法、定时器链表、空闲断开的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。