Linux 網絡編程的5種IO模型:信號驅動IO模型
背景
上一講 Linux 網絡編程的5種IO模型:多路復用(select/poll/epoll) 我們講解了多路復用等方面的知識,以及有關例程。
這一講我們來看 信號驅動IO 模型。
介紹
情景引入:
在信號驅動IO模型中,當用戶線程發起一個IO請求操作,會給對應的socket注冊一個信號函數,然后用戶線程會繼續執行,當內核數據就緒時會發送一個信號給用戶線程,用戶線程接收到信號之后,便在信號函數中調用IO讀寫操作來進行實際的IO請求操作。這個一般用於UDP中,對TCP套接口幾乎是沒用的,原因是該信號產生得過於頻繁,並且該信號的出現並沒有告訴我們發生了什么事情
在UDP上,SIGIO信號會在下面兩個事件的時候產生:
1 數據報到達套接字
2 套接字上發生錯誤
因此我們很容易判斷SIGIO出現的時候,如果不是發生錯誤,那么就是有數據報到達了。
而在TCP上,由於TCP是雙工的,它的信號產生過於頻繁,並且信號的出現幾乎沒有告訴我們發生了什么事情。因此對於TCP套接字,SIGIO信號是沒有什么使用的。
有關函數
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
關於有關內容的講解,請參考:Linux 系統編程 學習:進程間通信-Unix IPC-信號
例程
這對例程是不太規范的,因為有BUG。但因為這種消息模型用的比較少所以我就不改了。
server.c
#include <stdio.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
int listenfd1;
volatile int read_flag = 0;
static char buf[256] = { 0 };
void do_sigio(int sig)
{
struct sockaddr_in cli_addr;
int clifd, clilen;
read_flag = 1;
memset(buf, 0, sizeof(buf));
recvfrom(listenfd1, buf, sizeof(buf), 0, (struct sockaddr *)&cli_addr, &clilen);
printf("Listenfd1 Message %s \n", buf);
perror("recvfrom");
sendto(listenfd1, "Reply", sizeof("Reply"),0, (struct sockaddr *)&cli_addr, sizeof(cli_addr));
perror("sendto");
printf("sigio end\n");
read_flag = 0;
}
int main(int argc, char *argv[])
{
//綁定監聽7779端口的fd
struct sockaddr_in serv_addr;
listenfd1 = socket(AF_INET, SOCK_DGRAM, 0);
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(7779);
serv_addr.sin_addr.s_addr = INADDR_ANY;
struct sigaction sigio_action;
memset(&sigio_action, 0, sizeof(sigio_action));
sigio_action.sa_flags = 0;
sigio_action.sa_handler = do_sigio;
sigaction(SIGIO, &sigio_action, NULL);
fcntl(listenfd1, F_SETOWN, getpid());
int flags;
flags = fcntl(listenfd1, F_GETFL, 0);
flags |= O_ASYNC ;//| O_NONBLOCK;
fcntl(listenfd1, F_SETFL, flags);
bind(listenfd1, (struct sockaddr *) &serv_addr, sizeof(serv_addr));
while(1)
{
sleep(2);
}
close(listenfd1);
return 0;
}
client.c
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
int socketfd;
socklen_t n;
socketfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in serv_addr;
struct sockaddr_in addr;
bzero((char *)&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(7779);
char buf[64] = {0};
//write(socketfd, "client message", sizeof("client message"));
sendto(socketfd, "client message", sizeof("client message"),0, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
memset(buf, 0, sizeof(buf));
//read(socketfd, buf, sizeof(buf));
recvfrom(socketfd, buf, sizeof(buf), 0, (struct sockaddr *)&serv_addr, &n);
printf("%u %s\n",n ,buf);
return 0;
}
附錄:異步通知
ref :異步通知
注意:異步通知只有SIGIO信號,沒有別的信號可用,其他各種信號在app空間可以任意使用.
通過使用異步通知,應用程序可以在數據可用時收到一個信號,而無需不停地輪詢。
啟用步驟:
(1)它們指定一個進程作為文件的擁有者:使用 fcntl 系統調用發出 F_SETOWN 命令,這個擁有者進程的 ID 被保存在 filp->f_owner。目的:讓內核知道信號到達時該通知哪個進程。
(2)使用 fcntl 系統調用,通過 F_SETFL 命令設置 FASYNC 標志。
內核操作過程
1.F_SETOWN被調用時filp->f_owner被賦值。
-
當 F_SETFL 被執行來打開 FASYNC, 驅動的 fasync 方法被調用.這個標志在文件被打開時缺省地被清除。
-
當數據到達時,所有的注冊異步通知的進程都會被發送一個 SIGIO 信號。
Linux 提供的通用方法是基於一個數據結構和兩個函數,定義在。
數據結構:
struct fasync_struct{
int magic;
int fa_fd;
struct fasync_struct *fa_next;/* singly linked list */
struct file *fa_file;
};
驅動調用的兩個函數的原型:
int fasync_helper(int fd,structfile*filp,int mode, struct fasync_struct**fa);
void kill_fasync(struct fasync_struct**fa,int sig, int band);
當一個打開的文件的FASYNC標志被修改時,調用 fasync_helper 來從相關的進程列表中添加或去除文件。除了最后一個參數, 其他所有參數都時被提供給 fasync 方法的相同參數並被直接傳遞。 當數據到達時,kill_fasync 被用來通知相關的進程,它的參數是被傳遞的信號(常常是 SIGIO)和 band(幾乎都是 POLL_IN)。
這是 scullpipe 實現 fasync 方法的:
staticint scull_p_fasync(int fd,struct file*filp,int mode)
{
struct scull_pipe *dev = filp->private_data;
return fasync_helper(fd, filp, mode,&dev->async_queue);
}
當數據到達, 下面的語句必須被執行來通知異步讀者. 因為對 sucllpipe 讀者的新數據通過一個發出 write 的進程被產生, 這個語句出現在 scullpipe 的 write 方法中:
if (dev->async_queue) kill_fasync(&dev->async_queue, SIGIO, POLL_IN); /* 注意, 一些設備也針對設備可寫而實現了異步通知,在這個情況,kill_fasnyc 必須以 POLL_OUT 模式調用.*/
當文件被關閉時必須 調用fasync 方法,來從活動的異步讀取進程列表中刪除該文件。盡管這個調用僅當 filp->f_flags 被設置為 FASYNC 時才需要,但不管什么情況,調用這個函數不會有問題,並且是普遍的實現方法。 以下是 scullpipe 的 release 方法的一部分:
/* remove this filp from the asynchronously notified filp's */ scull_p_fasync(-1, filp, 0);
異步通知使用的數據結構和 struct wait_queue 幾乎相同,因為他們都涉及等待事件。區別異步通知用 struct file 替代 struct task_struct. 隊列中的 file 用獲取 f_owner, 一邊給進程發送信號。