嵌入式Linux網路程式設計,I/O多路複用,阻塞I/O模式,非阻塞I/O模式fcntl()/ioctl(),多路複用I/O select()/pselect()/poll(),訊號驅動I/O
文章目錄
1,I/O模型
在UNIX/Linux下主要有4種I/O 模型:
I/O模型 | 含義 |
---|---|
阻塞I/O | 最常用 |
非阻塞I/O | 可防止程序阻塞在I/O操作上,需要輪詢 |
I/O 多路複用 | 允許同時對多個I/O進行控制 |
訊號驅動I/O | 一種非同步通訊模型(當IO有事件的時候,在應用程式中會收到一個訊號SIGIO,可以對訊號安裝一個處理控制代碼,就可以對訊號實現非同步的處理) |
2,阻塞I/O 模式
- 阻塞I/O 模式是最普遍使用的I/O 模式,大部分程式使用的都是阻塞模式的I/O 。
- 預設情況下,套接字建立後所處於的模式就是阻塞I/O 模式。
- 前面學習的很多讀寫函式在呼叫過程中會發生阻塞。
·讀操作中的read、recv、recvfrom
·寫操作中的write、send
·其他操作:accept、connect
2.1,讀阻塞(以read函式為例)
- 程序呼叫read函式從套接字上讀取資料,當套接字的接收緩衝區中還沒有資料可讀,函式read將發生阻塞。
- 它會一直阻塞下去,等待套接字的接收緩衝區中有資料可讀。
- 經過一段時間後,緩衝區內接收到資料,於是核心便去喚醒該程序,通過read訪問這些資料。
- 如果在程序阻塞過程中,對方發生故障,那這個程序將永遠阻塞下去。
2.2,寫阻塞
- 在寫操作時發生阻塞的情況要比讀操作少。主要發生在要寫入的緩衝區的大小小於要寫入的資料量的情況下。
- 這時,寫操作不進行任何拷貝工作,將發生阻塞。
- 一量傳送緩衝區內有足夠的空間,核心將喚醒程序,將資料從使用者緩衝區中拷貝到相應的傳送資料緩衝區。
- UDP不用等待確認,沒有實際的傳送緩衝區,所以UDP協議中不存在傳送緩衝區滿的情況,在UDP套接字上執行的寫操作永遠都不會阻塞。
3,非阻塞模式I/O
- 當我們將一個套接字設定為非阻塞模式,我們相當於告訴了系統核心:“當我請求的I/O 操作不能夠馬上完成,你想讓我的程序進行休眠等待的時候,不要這麼做,請馬上返回一個錯誤給我。”
- 當一個應用程式使用了非阻塞模式的套接字,它需要使用一個迴圈來不停地測試是否一個檔案描述符有資料可讀(稱做polling)。
- 應用程式不停的polling 核心來檢查是否I/O操作已經就緒。這將是一個極浪費CPU 資源的操作。
- 這種模式使用中不普遍。
3.1,非阻塞模式的實現(fcntl()函式、ioctl() 函式)
當你一開始建立一個套接字描述符的時候,系統核心將其設定為阻塞IO模式。
可以使用函式fcntl()設定一個套接字的標誌為O_NONBLOCK 來實現非阻塞。
程式碼實現;
3.1.1,fcntl( )函式
int fcntl(int fd, int cmd, long arg);
int flag;
flag = fcntl(sockfd, F_GETFL, 0);
flag |= O_NONBLOCK;
fcntl(sockfd, F_SETFL, flag);
3.1.2,ioctl() 函式
int b_on =1;
ioctl(sock_fd, FIONBIO, &b_on);
4,多路複用I/O
- 應用程式中同時處理多路輸入輸出流,若採用阻塞模式,將得不到預期的目的;
- 若採用非阻塞模式,對多個輸入進行輪詢,但又太浪費CPU時間;
- 若設定多個程序,分別處理一條資料通路,將新產生程序間的同步與通訊問題,使程式變得更加複雜;
- 比較好的方法是使用I/O多路複用。其基本思想是:
·先構造一張有關描述符的表(fd_set),然後呼叫一個函式(select()/poll())。當這些檔案描述符中的一個或多個已準備好進行I/O時函式才返回。
·函式返回時告訴程序那個描述符已就緒,可以進行I/O操作。 - 多路複用不止針對套接字fd,也針對普通的檔案描述符fd
4.1,實現多路複用 select()/poll()
4.1.1,實現多路複用 select()
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *read_fds, fd_set *write_fds, fd_set *except_fds, struct timeval *timeout);
引數 | 含義 |
---|---|
nfds | 所有監控的檔案描述符中最大的那一個加1(maxfd+1) |
read_fds | 所有要讀的檔案檔案描述符的集合 |
write_fds | 所有要的寫檔案檔案描述符的集合(一般填NULL) |
except_fds | 其他要向我們通知的檔案描述符(異常集合,如:帶外資料。一般填NULL) |
timeout | 超時設定. Null:一直阻塞,直到有檔案描述符就緒或出錯 時間值為0:僅僅檢測檔案描述符集的狀態,然後立即返回 時間值不為0:在指定時間內,如果沒有事件發生,則超時返回。 |
struct timeval {
long tv_sec; /* seconds 秒*/
long tv_usec; /* microseconds 微妙*/
};
1秒(s) = 103毫秒(ms) = 106微妙(us) = 109納秒(ns) = 1012皮秒(ps)
- 在我們呼叫select時程序會一直阻塞直到以下的一種情況發生.
·有檔案可以讀.
·有檔案可以寫.
·超時所設定的時間到. - 為了設定檔案描述符我們要使用幾個巨集:
巨集 | 形式 | 含義 |
---|---|---|
FD_SET | void FD_SET(int fd,fd_set *fdset) | 將fd加入到fdset |
FD_CLR | void FD_CLR(int fd,fd_set *fdset) | 將fd從fdset裡面清除 |
FD_ZERO | void FD_ZERO(fd_set *fdset) | 從fdset中清除所有的檔案描述符 |
FD_ISSET | int FD_ISSET(int fd,fd_set *fdset) | 判斷fd是否在fdset集合中 |
4.1.2,另外的函式:pselect()/poll()
- int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
·注意引數類似struct timespec 和sigset_t
·select()增強
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
- int poll(struct pollfd *fds, nfds_t nfds, int timeout);
·類似select(),稍節省空間
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
常量(events/revents) | 說明 |
---|---|
POLLIN | 普通或優先順序帶資料可讀,有資料可讀 |
POLLRDNORM | 普通資料可讀,有普通資料可讀 |
POLLRDBAND | 優先順序帶資料可讀,有優先資料可讀 |
POLLPRI | 高優先順序資料可讀, 有緊迫資料可讀 |
POLLOUT | 普通資料可寫, 寫資料不會導致阻塞 |
POLLWRNORM | 普通資料可寫, 寫普通資料不會導致阻塞 |
POLLWRBAND | 優先順序帶資料可寫,寫優先資料不會導致阻塞 |
POLLMSGSIGPOLL | 訊息可用 |
POLLER | 發生錯誤 |
POLLHUP | 發生掛起 |
POLLNVAL | 描述字不是一個開啟的檔案 |
- 第二個引數nfds:要監視的描述符的數目。
- 最後一個引數timeout:是一個用毫秒錶示的時間,是指定poll在返回前沒有接收事件時應該等待的時間。如果 它的值為-1,poll就永遠都不會超時。如果整數值為32個位元,那麼最大的超時週期大約是30分鐘。
4.1.3,另外的函式:epoll介面
epoll的介面非常簡單,一共就三個函式:
- int epoll_create(int size);
建立一個epoll的控制代碼,size用來告訴核心這個監聽的數目一共有多大。這個引數不同於
select()中的第一個引數,給出最大監聽的fd+1的值。需要注意的是,當建立好epoll控制代碼後,它就
是會佔用一個fd值,在linux下如果檢視/proc/程序id/fd/,是能夠看到這個fd的,所以在使用完epoll
後,必須呼叫close()關閉,否則可能導致fd被耗盡。
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件註冊函式,它不同與select()是在監聽事件時告訴核心要監聽什麼型別的事件,而
是在這裡先註冊要監聽的事件型別。第一個引數是epoll_create()的返回值,第二個引數表示動
作,用三個巨集來表示:
巨集(動作) | 含義 |
---|---|
EPOLL_CTL_ADD | 註冊新的fd到epfd中 |
EPOLL_CTL_MOD | 修改已經註冊的fd的監聽事件 |
EPOLL_CTL_DEL | 從epfd中刪除一個fd |
第三個引數是需要監聽的fd,第四個引數是告訴核心需要監聽什麼事,struct epoll_event結
構如下
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
巨集(enents) | 含義 |
---|---|
EPOLLIN | 表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉) |
EPOLLOUT | 表示對應的檔案描述符可以寫 |
EPOLLPRI | 表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來) |
EPOLLERR | 表示對應的檔案描述符發生錯誤 |
EPOLLHUP | 表示對應的檔案描述符被結束通話 |
EPOLLET | 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的 |
EPOLLONESHOT | 只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL佇列裡 |
- int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 引數
epoll_wait的功能與select()類似,它等待_epfd_所代表的epoll例項中監聽的事件發生。
_events_指標返回已經準備好的事件,最多有_maxevents_個,引數_maxevent_必須大於零。
_timeout_引數指定epoll_wait函式阻塞的毫秒數的最小值(精度和系統時鐘有關,核心排程也會
對此造成一些影響),設定_timeout_為-1則epoll_wait()會一直阻塞,設定為0則會立即返回。
- 返回值
如果函式呼叫成功,epoll_wait()函式返回已經準備好進行所要求的I/O操作的檔案描述符的數
量,如果在_timeout_時間內沒有描述符準備好則返回0。出錯時,epoll_wait()返回-1並且把errno
設定為對應的值
5,TCP多路複用
TCP多路複用I/O | 關鍵點 |
---|---|
1. select( )函式裡面的各個檔案描述符fd_set集合的引數在select( )前後發生了變化: 前:表示關心的檔案描述符集合 後:有資料的集合(如不是在超時還回情況下) 2. kernel使fd_set集合發生了變化 3. 若是監聽套接字上有資料,則有新客戶端連線,就去呼叫accept()函式 4. 若是已建立連線的套接字上有資料,則去讀資料 |
int main(void)
{
fd_set rset;
int maxfd = -1;
struct timeval tout;
fd = socket(...);
bind(fd,...);
listen(fd,...);
while(1)
{
maxfd = fd;
FD_ZERO(&rset);
FD_SET(fd,&rset);
/*依次把已經建立好連線的fd加入到集合中,記錄下來最大的檔案描述符maxfd*/
#if 0
select(maxfd+1,&rset,NULL,NULL,NULL);
#else
struct timeval tout;
tout.tv_sec = 5;
tout.tv_usec = 0;
select(maxfd+1,&rset,NULL,NULL,&tout);
#endif
int newfd;
if(FD_ISSET(fd,&rset))//依次判斷
{
newfd = accept(fd,...);//若是監聽套接字上有資料,則有新客戶端連線,就去呼叫accept()函式
}
/* 若是已建立連線的套接字上有資料,則去讀資料 */
/* ... */
}
}
6,IO複用select()示例
6.1 select()—net.h
#ifndef __NET_H__
#define __NET_H__
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/select.h>
#define SERV_IP_ADDR "192.168.31.100"
#define SERV_PORT 5002
#define BACKLOG 5
#define QUIT_STR "quite"
#define SERV_RESP_STR "Server:"
#endif
6.2 select()—client.c
/* ./client serv_ip serv_port */
#include "net.h"
void usage(char *s)
{
printf("Usage: %s <serv_ip> <serv_port>\n",s);
printf("\tserv_ip: server ip address\n");
printf("\tserv_port: server port(>5000)\n ");
}
int main(int argc, const char *argv[])
{
int fd;
short port;
struct sockaddr_in sin;
if(argc != 3)
{
usage((char *)argv[0]);
exit(1);
}
if((port = atoi(argv[2])) < 5000)
{
usage((char *)argv[0]);
exit(1);
}
/* 1 建立socket fd */
if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket");
exit(-1);
}
/* 2 連線伺服器 */
/* 2.1 填充struct sockaddr_in結構體變數*/
bzero(&sin,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(port);//轉為網路位元組序埠號
if(inet_pton(AF_INET,argv[1],(void *)&sin.sin_addr.s_addr) < 0)
{
perror("inet_pton");
goto _error1;
}
/* 2.2 連線伺服器*/
if(connect(fd,(struct sockaddr *)&sin,sizeof(sin)) < 0)
{
perror("connect");
goto _error1;
}
printf("client staring ... OK!\n");
fd_set rset;
int maxfd;
struct timeval tout;
char buf[BUFSIZ];
int ret = -1;
while(1)
{
FD_ZERO(&rset);
FD_SET(0,&rset);
FD_SET(fd,&rset);
maxfd = fd;
tout.tv_sec = 5;
tout.tv_usec = 0;
select(maxfd+1,&rset,NULL,NULL,&tout);
if(FD_ISSET(0,&rset))//標準輸入裡面是不是有輸入
{
/* 讀取鍵盤輸入,傳送到網路套接字fd */
bzero(buf,BUFSIZ);
do
{
ret = read(0,buf,BUFSIZ-1);
}while(ret <0 && EINTR == errno);
if(ret < 0)
{
perror("read");
continue ;
}
if(ret == 0)//沒讀到資料
{
continue;
}
if(write(fd,buf,strlen(buf)) < 0)
{
perror("write() to socket");
continue ;
}
if(strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)) == 0)//退出在傳送之後
{
printf("client is existing!\n");
break;
}
}
if(FD_ISSET(fd,&rset))//伺服器傳送了資料過來
{
/* 讀取套接字資料,處理 */
bzero(buf,BUFSIZ);
do
{
ret = read(fd,buf,BUFSIZ-1);
}while(ret <0 && EINTR == errno);
if(ret < 0)
{
perror("read from socket");
continue ;
}
if(ret == 0)//從套接字中讀到的資料個數小於0,說明伺服器關閉
{
break ;
}
printf("server said: %s",buf);
if((strlen(buf) > strlen(SERV_RESP_STR)) && strncasecmp(buf+strlen(SERV_RESP_STR),QUIT_STR,strlen(QUIT_STR)) == 0)
{
printf("sender client is existing!\n");
break;
}
}
}
_error1:
close(fd);
return 0;
}
6.3 select()—sever.c
#include "net.h"
#include "linklist.h"
#include <sys/ioctl.h>
/* 執行緒傳參 */
typedef struct{
int addr;//客戶端IP地址
int port;//客戶端埠號
int fd;//為請求連結的客戶端分配的新的socket fd
}ARG;
/* IO多路複用select()處理函式 */
void do_select(int fd);
int main(int argc, const char *argv[])
{
int fd;
struct sockaddr_in sin;//如果是IPV6的程式設計,要使用struct sockddr_in6結構體(詳細情況請參考man 7 ipv6),通常更通用的方法可以通過struct sockaddr_storage來程式設計
/* 1 建立socket fd */
if((fd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket");
exit(-1);
}
/* 優化 1 允許繫結地址快速重用 */
int b_reuse = 1;
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&b_reuse,sizeof(int));
/* 2 繫結 */
/* 2.1 填充struct sockaddr_in 結構體變數*/
bzero(&sin,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(SERV_PORT);
#if 1
/* 優化 2 讓伺服器可以繫結在任意的IP上*/
sin.sin_addr.s_addr = htonl(INADDR_ANY);
#else
if(inet_pton(AF_INET,SERV_IP_ADDR,(void *)&sin.sin_addr.s_addr) < 0)
{
perror("inet_pton");
goto _error1;
}
#endif
/* 2.2 繫結*/
if(bind(fd,(struct sockaddr *)&sin,sizeof(sin)))
{
perror("bind");
goto _error1;
}
/* 3 使用listen()把主動套接字變成被動套接字 */
if(listen(fd,BACKLOG) < 0)
{
perror("listen");
goto _error1;
}
do_select(fd);
_error1:
close(fd);
return 0;
}
void do_select(int fd)
{
linklist fdlist,sin_list;//建立一個列表,用於檔案描述符及客戶端資訊儲存
fdlist = create_linklist();
datatype sin_data;//每個物件包括客戶端的socket fd,ipv4地址,埠號
sin_data.fd = fd;
int maxfd = fd;
//struct timeval tout = {5,0};
insert_end_linklist(fdlist,sin_data);//將lsten()處理後的fd加入列表
//show_linklist(fdlist);
fd_set rset;
int newfd = -1;
int ret = -1;
char buf[BUFSIZ];//BUFSIZ是系統提供的
char resp_buf[BUFSIZ+10];
struct sockaddr_in cin;
socklen_t cin_addr_len = sizeof(cin);
/* 用select()函式實現I/O多路複用*/
while(1)
{
int i;
FD_ZERO(&rset);
if(get_length_linklist(fdlist) >= 1)//將列表中的fd加入讀集合進行處理
{
//puts("11111111111111111111111111111");
for(i=0;i<get_length_linklist(fdlist);i++)
{
sin_list = get_list_pos_linklist(fdlist,i);
sin_data = sin_list->data;
FD_SET(sin_data.fd,&rset);
maxfd = sin_data.fd > maxfd ? sin_data.fd : maxfd;
//printf("第 %d 個(fd:%d)(ip:%s)(port:%d)\n",i,sin_data.fd,sin_data