您的位置:首页 > 理论基础 > 计算机网络

网络模型

2016-05-11 16:33 525 查看
套接字socket通信属于网络的传输层通信,通常都是由操作系统实现,主要有两种tcp和udp两种(分别实现的tcp协议和udp协议),本文诉述的网络模型都是建立在Linux操作系统实现的套接字API基础上。(套接字实现进程之间的通信)


一 、Linux 5种 I/O模型

网络数据I/O 操作拥有两个阶段,以读数据操作为例:1 操作系统读取网络数据成功放入系统内核缓冲区(或者说tcp/udp缓冲区,因为操作系统这网络模型中实现是传输层协议), 2 系统内核缓冲区数据被拷贝到应用程序缓冲区(即通常我们的应用程序中定义的数据空间)

如果已经了解常见的网络套接字编程函数,可跳过1.1-1.5


1.1阻塞I/O

应用程序调用阻塞的I/O函数(recv()/recvfrom()),进程会一直阻塞,直到数据拷贝到用户缓冲区成功。



代码示例1 (完整代码示例见附录1): 实现发送数据到服务端,并读取服务端的返回数据

{

/* 从标准输入设备取得字符串*/

len =read(STDIN_FILENO,buffer,sizeof(buffer));

/* 将字符串传送给server端*/

sendto(s,buffer,len,0,(struct sockaddr *)&addr,addr_len);

/* 接收server端返回的字符串*/

len = recvfrom(s,buffer,sizeof(buffer),0,(struct sockaddr *)&addr,&addr_len);

printf("receive: %s",buffer);

}

以recvfrom 函数为例,当应用程序调用此函数时,应用程序会进入内核态,等待数据准备完成(此过程 应该是操作系统读取tcp/udp 数据包过程,先读取固定大小的tcp报文头部,然后根据头部指定的包体大小读取完整tcp报文), 数据准备完成后,由操作系统负责将数据拷贝到用户应用程序的缓冲区(即 程序中定义的buffer缓冲区),并切换到用户态。

(用户态 :可简单理解为运行用户的代码例如上面的示例代码 内核态:是操作系统,运行自己的代码)


1.2非阻塞I/O



与阻塞I/O模型不同,非阻塞I/O 需要设置套接字(示例代码中 变量s 为非阻塞模式 fcntl(s, F_SETFL, O_NONBLOCK)), 在调用 recvfrom 函数后,内核判断无数据准备好(内核缓冲区即tcp/udp缓冲区未读取到完整tcp报文),则直接切换到用户态,并返回EWOULDBLOCK,如图6.2 中,此种模型需要应用程序不断调用recvfrom函数直到成功读取数据,在内核态与用户态之间的不断切换会消耗cpu时间。


1.3 I/O复用(select + epoll)



select 函数原型:

int select(int nfds, fd_set *readset, fd_set *writeset,fd_set* exceptset, struct tim *timeout);

此函数的最后一个参数timeout 决定了select系统调用是否阻塞或者阻塞多久。

1. timeout=NULL(阻塞:直到有一个fd位被置为1函数才返回) 类似 1.1 recvfrom阻塞

2. timeout所指向的结构设为非零时间(等待固定时间:有一个fd位被置为1或者时间耗尽,函数均返回)

3. timeout所指向的结构,时间设为0(非阻塞:函数检查完每个fd后立即返回)

注意select系统调用的阻塞与其监控的套接字(readset writeset集合中套接字)阻塞性质无关,由参数timeout决定,另外select函数在内核态,只判断有无数据准备好(即网络数据tcp/udp包是否到达系统tcp/udp缓存区), 无1.1 中介绍的函数返回时将数据从系统缓存区拷贝到用户缓冲区过程。

(对于网上很多地方 评论select函数是阻塞的 是不全面的)

在select 函数成功返回(>0) 表时某些套接字可读时,再使用recvfrom函数读取套接字上面的数据(如1.1,1.2 描述,通常这里会将套接字设置成阻塞模式,调用recvfrom函数)

代码实例:

while(1)

{

FD_ZERO(&fds); //每次循环都要清空集合,否则不能检测描述符变化

/*

select函数 每次返回会清除 参数readset

和 wrietset中没有准备好的套接字,所以每次轮询都需要重新添加监控集合中

*/

FD_SET(sock,&fds); //添加描述符

FD_SET(fp,&fds); //同上

maxfdp=sock>fp?sock+1:fp+1; //描述符最大值加1

switch(select(maxfdp,&fds,&fds,NULL,&timeout)) //select使用

{

case -1: exit(-1);break; //select错误,退出程序

case 0:break; // 没有数据准备好

default:

if(FD_ISSET(sock,&fds)) //测试sock是否可读,即是否网络上有数据

{

/*由于套接字sock上面数据已经准备好,

这里会将数据从内核缓冲区拷贝到用户缓冲区buffer*

*/

recvfrom(sock,buffer,256,.....);

}// end if break;

}// end switch

}//end while

}//end main

缺点:

1 数据需要由应用程序在得知数据准备好后,重新切换到内核态,拷贝数据到用户态缓冲区

2 需要循环遍历寻找具体是哪些描述符上数据准备就绪



注意 linux 2.6 版本后epoll_wait采用了mmap(mmap 内存映射 简单说就是将文件或者对象映射到一块内存上,方便多个进程之间共享和直接操作,无需通常读取文件时的先内核态读取,再拷贝用户态缓冲区过程),这样当epoll_wait 在网络数据准备就绪后返回时,其内核态数据已经拷贝到用户缓冲区。

epoll 主要解决了select模型的两个缺点(具体见1.3节),尤其是第二个缺点,在有大量并发连接,且只有少数活跃的情况下的效率问题。

epoll 使用主要有以下三个函数:

 1、int epoll_create(int size)

创建一个epoll句柄,参数size用来告诉内核监听的数目。

/* event 包含了需要监听的时间以及用来保存数据的用户缓冲区*/

2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

epoll事件注册函数,(epfd 是函数1 返回的epoll句柄)

/* epoll_wait函数返回时,events集合中包含了准备就绪的套接字以及数据)*/

3、 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)

等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,timeout(0 非阻塞 -1 阻塞)

和select不同 epoll_wait 返回时,events集合中已经包括了就绪事件以及对应的数据。换句话说 epoll_wait在返回时 已经完成数据从内核缓冲区拷贝到用户缓冲区工作(由于epoll_wait返回时已经知道哪些套接字上数据准备就绪,无需select返回后 仍需轮询确认哪些套接字上准备就绪,同时也无需再调用函数recvfrom复制数据到用户缓冲区)。


1.4信号渠道I/O(SIGIO)



1 首先设置套接字可进行套接字信号驱动I/O(ioctl) 和设置套接字非阻塞性质类似。

2 通过系统调用(sigaction),指定信号处理程序(通常会让主程序睡眠等待并接受信号处理)

3 信号到达时,当前进程会被中断或者由睡眠状态唤醒然后调用信号处理函数(该信号处理函数通常可以直接处理网络数据或者通过其他状态位通知主程序数据已经准备好)


1.5异步I/O



异步I/O 是linux 2.6 新增的一个特性。aio_read 函数和(epoll类似)会传递给内核一个用户缓冲区,内核会在数据准备就绪后将数据复制到用户缓冲区中后 发送SIGIO信号给当前进程,或者调用aio_read函数调用中指定的回调函数。

这里和之前(1.1-1.4)显然不同,aio_read函数调用后,会立即返回,后续无需轮询查询网络数据是否准备就绪,在网络数据准备就绪后(且已拷贝到用户缓冲区)时,会自动调用之前调用aio_read函数时指定的回调函数地址(执行回调函数是主进程中的另一个线程)或者发送信号给当前进程(当前进程会被中断进入信号处理进程 由主进程主线程负责执行信号函数)。


二 、阻塞,非阻塞,同步,异步概念总结

通过对1.1,1.2,1.3 的理解可以归纳阻塞和非阻塞是系统在调用系统调用(recvfrom,select)的时候函数的实现方式而已。

阻塞:调用某个函数(recvfrom select[timeout=NULL]) 由于数据未准备好或者其他原因,当前进程会挂起进入睡眠

非阻塞:调用某个函数(recvfrom select[timeout=NULL]) 由于数据未准备好或者其他原因,当前进程并不进入睡眠,而是立即返回一个状态反应此种情况给用户程序

非阻塞缺点:通常需要用户轮询调用某个函数直到读取到数据,导致用户态和系统态之间的来回切换,消耗cpu资源,

阻塞缺点:函数不能立即返回,用户进程进入睡眠状态,无法处理其他事情(内核态轮询数据是否准备就绪)

通过对1.3,1.4,1.5 可以归纳同步和异步是相对于用户程序和操作系统内核的交互方式而言的.

同步: 应用程序轮询查看是否准备就绪 (用户态切换内核态 轮询)

异步:内核在IO事件发生的时候通知应用程序(信号或者回调函数)。

同步和阻塞区别: 同步是用户态程序 轮询调用系统调用(select epoll_wait)查看数据是否准备就绪,阻塞是用户程序调用一个系统调用(recvfrom)后,切换到内核态,在内核态中不断轮询查询数据是否准备就绪。

由于同步 是由用户态不断切换到内核态的轮询,因此证明了网上的说法同步是可以被用户信号中断的。(内核态不能接受用户信号)

异步和同步的区别:异步没有类似同步(select, epoll_wait)那样的应用程序轮询调用某个系统调用,而是在数据准备好,发送信号给当前进程或者开启新线程调用回调函数。(对于网上有人将epoll模型归纳为也异步,其理由是在用户进程得到数据就绪通知的同时,数据已经拷贝到用户缓冲区,此种说法不妥)

异步和非阻塞区别:两者并无直接关联。

综述:阻塞和非阻塞是某个函数内部的实现方式,同步和异步在网络模型是描述用户程序和操作系统之间的交互方式的。同步模型(select epoll)中可以有非阻塞的函数调用,也可以有阻塞函数调用(因此有人将I/O复用模型归纳为同步阻塞模型,也是合理的,因为select和recvfrom在这个I/O模型里选择了阻塞调用方式),异步I/O模型 在用户态和系统态都没有轮询操作,而是在网络数据准就绪后通过异步的方式处理(开启线程调用回调函数或者发送信号给当前进程)。

通过以上分析: 信号渠道I/O 可以归纳为 异步阻塞模型(网络有类似归纳方法) (设置完SIGIO信号处理程序后,无需用户进程轮询 这是异步的概念,数据到达就绪后,信号处理程序中在阻塞套接字上调用recvfrom函数读取实际数据,阻塞概念)

Select和epoll 可以归为同步阻塞 同步:因为都需要用户进程轮询(select epoll_wait) 阻塞:因为select epoll_wait 在这个I/O模型中通常都是设置一定的阻塞时间,且在select调用之后,会阻塞调用recvfrom或者write等函数。


三 、附录

网址

http://wiki.babel.baidu.com/twiki/bin/view/Ps/Ns/AsynchronousAndSimultaneous (垃圾)

/article/5948078.html

/article/5815629.html

/article/5903732.html (proactor 和 reactor模式)

http://blog.chinaunix.net/uid-14874549-id-3487338.html

http://blog.chinaunix.net/uid-26669729-id-3077015.html

/article/2398619.html (信号I/O)

书籍推荐

unix 环境高级编程

windows 网络编程 (第八章讲解了windows下的5种网络模型)

unix网络编程 (两卷)

tcp/ip 详解卷1

ACE中间件 (封装了window和linux 下的网络函数的开源网络编程工具包,同时提供其他例如日志,线程池等功能,对设计模式运用很全面,上下两本)


4.1 Linux下阻塞套接字使用示例


4.2 Linux信号I/O模型

/article/2398619.html

注意sigsupend 函数会实得当前函数进入睡眠,同时可接受信号


4.3 Linux AIO 异步I/O模型

http://blog.chinaunix.net/uid-52437-id-2108857.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: