1. 程式人生 > >嵌入式Linux網路程式設計,I/O多路複用,阻塞I/O模式,非阻塞I/O模式fcntl()/ioctl(),多路複用I/O select()/pselect()/poll(),訊號驅動I/O

嵌入式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 模式

  1. 阻塞I/O 模式是最普遍使用的I/O 模式,大部分程式使用的都是阻塞模式的I/O 。
  2. 預設情況下,套接字建立後所處於的模式就是阻塞I/O 模式。
  3. 前面學習的很多讀寫函式在呼叫過程中會發生阻塞。
    ·讀操作中的read、recv、recvfrom
    ·寫操作中的write、send
    ·其他操作:accept、connect

2.1,讀阻塞(以read函式為例)

  1. 程序呼叫read函式從套接字上讀取資料,當套接字的接收緩衝區中還沒有資料可讀,函式read將發生阻塞。
  2. 它會一直阻塞下去,等待套接字的接收緩衝區中有資料可讀。
  3. 經過一段時間後,緩衝區內接收到資料,於是核心便去喚醒該程序,通過read訪問這些資料。
  4. 如果在程序阻塞過程中,對方發生故障,那這個程序將永遠阻塞下去。

2.2,寫阻塞

  1. 在寫操作時發生阻塞的情況要比讀操作少。主要發生在要寫入的緩衝區的大小小於要寫入的資料量的情況下。
  2. 這時,寫操作不進行任何拷貝工作,將發生阻塞。
  3. 一量傳送緩衝區內有足夠的空間,核心將喚醒程序,將資料從使用者緩衝區中拷貝到相應的傳送資料緩衝區。
  4. UDP不用等待確認,沒有實際的傳送緩衝區,所以UDP協議中不存在傳送緩衝區滿的情況,在UDP套接字上執行的寫操作永遠都不會阻塞。

3,非阻塞模式I/O

  1. 當我們將一個套接字設定為非阻塞模式,我們相當於告訴了系統核心:“當我請求的I/O 操作不能夠馬上完成,你想讓我的程序進行休眠等待的時候,不要這麼做,請馬上返回一個錯誤給我。”
  2. 當一個應用程式使用了非阻塞模式的套接字,它需要使用一個迴圈來不停地測試是否一個檔案描述符有資料可讀(稱做polling)。
  3. 應用程式不停的polling 核心來檢查是否I/O操作已經就緒。這將是一個極浪費CPU 資源的操作。
  4. 這種模式使用中不普遍。

在這裡插入圖片描述

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

  1. 應用程式中同時處理多路輸入輸出流,若採用阻塞模式,將得不到預期的目的;
  2. 若採用非阻塞模式,對多個輸入進行輪詢,但又太浪費CPU時間;
  3. 若設定多個程序,分別處理一條資料通路,將新產生程序間的同步與通訊問題,使程式變得更加複雜;
  4. 比較好的方法是使用I/O多路複用。其基本思想是:
    ·先構造一張有關描述符的表(fd_set),然後呼叫一個函式(select()/poll())。當這些檔案描述符中的一個或多個已準備好進行I/O時函式才返回。
    ·函式返回時告訴程序那個描述符已就緒,可以進行I/O操作。
  5. 多路複用不止針對套接字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)

  1. 在我們呼叫select時程序會一直阻塞直到以下的一種情況發生.
    ·有檔案可以讀.
    ·有檔案可以寫.
    ·超時所設定的時間到.
  2. 為了設定檔案描述符我們要使用幾個巨集:
巨集 形式 含義
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 描述字不是一個開啟的檔案
  1. 第二個引數nfds:要監視的描述符的數目。
  2. 最後一個引數timeout:是一個用毫秒錶示的時間,是指定poll在返回前沒有接收事件時應該等待的時間。如果 它的值為-1,poll就永遠都不會超時。如果整數值為32個位元,那麼最大的超時週期大約是30分鐘。

4.1.3,另外的函式:epoll介面

epoll的介面非常簡單,一共就三個函式:

  1. int epoll_create(int size);

建立一個epoll的控制代碼,size用來告訴核心這個監聽的數目一共有多大。這個引數不同於
select()中的第一個引數,給出最大監聽的fd+1的值。需要注意的是,當建立好epoll控制代碼後,它就
是會佔用一個fd值,在linux下如果檢視/proc/程序id/fd/,是能夠看到這個fd的,所以在使用完epoll
後,必須呼叫close()關閉,否則可能導致fd被耗盡。

  1. 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佇列裡
  1. int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  1. 引數

epoll_wait的功能與select()類似,它等待_epfd_所代表的epoll例項中監聽的事件發生。
_events_指標返回已經準備好的事件,最多有_maxevents_個,引數_maxevent_必須大於零。
_timeout_引數指定epoll_wait函式阻塞的毫秒數的最小值(精度和系統時鐘有關,核心排程也會
對此造成一些影響),設定_timeout_為-1則epoll_wait()會一直阻塞,設定為0則會立即返回。

  1. 返回值

如果函式呼叫成功,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