一、介绍
网络IO的本质就是socket流的读取,通常一次IO读取会涉及两个阶段与两个对象,其中两个对象为:用户进程(线程)Process(Thread)、内核对象(kernel),两个阶段为:等待流数据准备阶段、从内核向进程复制数据阶段。
对于socket而言,第一步通常等待网络上的数据分组到达,然后被复制到内核的某个缓冲区,第二步数据从内核的缓冲区复制到应用进程的缓冲区。
I/O模型可细分为五种类型:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO、异步IO。
二、简单的I/O模型的特点
1、阻塞I/O:
最常用、最简单、效率最低
2、非阻塞I/O:
可防止进程阻塞在I/O操作上,需要轮询
3、 I/O 多路复用:(select,poll,epoll)
允许同时对多个I/O进行控制
4、 信号驱动I/O:
一种异步通信模型
三、阻塞IO模型
首先,在linux系统中默认所有的IO都是阻塞IO。
阻塞IO的特点是从kernel读取数据时信号并未立刻返回,而是等待数据到达完毕或发生错误才会返回结
果,这个过程是阻塞的。
术语描述:当用户进程调用recvfrom这个系统调用时,kernel就开始等待数据到来,而进程这边会处于阻塞状态。当kernel将数据准备好后,就会将数据拷贝到用户进程的缓冲区,然后kernel返回结果,用户进程才会解除block状态,重新运行起来。
四、非阻塞IO模型
与阻塞IO不同,当用户进程发出recvfrom调用时,如果kernel中数据还没有准备好,那么并不会block用户进程,而是会返回error错误。相对于用户进程而言,每次发送读取操作后,并不需要等待,而是会立刻返回结果,当收到error时,就知道数据未准备完毕,然后继续发送读取操作,直到可以直接读取数据到缓冲区为止。虽然在执行read请求操作时,用户进程并未阻塞,但是当recvfrom将数据从内核拷贝到进程时,用户进程处于阻塞状态。
五、I/O的多路复用
产生原因: 在具有大量IO请求的场景下,需要应用进程创建多个线程去读取数据,每个线程都会调用recvfrom去读取数据。在这种高并发的情况下,可能进程需要创建成千上万个线程,增加服务器负荷,并且造成了严重的资源浪费。
因此,有了多路复用IO模型,使用一个线程去监听多个网络请求,即文件描述符,这样就实现了使用少量线程对大量请求进行监听,然后再让对应的线程进行数据读取。那么目前常用select、poll、epoll函数对fd文件描述符进行监听。
多路复用IO又称事件驱动IO,进程使用IO多路复用在两个阶段都是阻塞的状态,进程使用select函数,其中select函数有一个参数是文件描述符集合,对这些文件描述符进行监听,当文件描述符就绪时,会返回readable信号,然后用户进程调用recvfrom进行读取数据,由于可同时监听多个IO,效率比阻塞IO高。
select
进程调用select后会被阻塞,将需要监听的文件描述符放入fd_set,并将fd_set复制到内核空间,内核空间会对fd_set进行轮询遍历,若无mark值,则会暂时挂起等待超时时间之后继续轮询,直到有数据准备就绪。最后将fd_set复制回用户进程,进行读/写操作。
复杂度O(n)
select的缺点:
1.select监控数量受限
select能监控的fd数量有上限,32位系统一般为1024,64位系统为2048,这个上限可以通过修改参数提高,但是相应的会损失性能。
2.轮询效率低
对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。
3.频繁拷贝复杂,开销大
需要维护一个用来存放大量fd的数据结构,用户空间需要维护一个fd_set,fd_set的每一位都表示一个文件描述符,开始时会将其发给内核,这会使得用户空间和内核空间在传递该结构时复制开销大。
4.select是水平触发
应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作。那么之后select调用还是会将这些文件描述符返回,通知进程。
#include <stdio.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#define SERV_ip "0"
#define SERV_PORT 6666
//命令后模式下: gg=GG 调格式
int server_init(char *ip, int port, int num)
{
int listenfd, ret;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == listenfd)
{
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = inet_addr(ip);
ret = bind(listenfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(-1 == ret)
{
perror("bind");
exit(-1);
//goto ERROR_STEM;
}
listen( listenfd, num );
return listenfd;
#if 0
ERROR_STEM:
close(listenfd);
exit(-1);
#endif
}
#define SIZE 128
int main(int argc,const char *argv[])
{
int listenfd, connfd, ret , i;
listenfd = server_init(SERV_ip, SERV_PORT, 8);
if(-1 == listenfd)
{
perror("server_init ");
exit(-1);
}
//通过io多路复用的select 解决 阻塞问题
char buf[SIZE] = {0};
fd_set rfds, fds; //定义读集合
FD_ZERO(&rfds); //清空读集合
//将需要的描述符加入读集合
FD_SET(0, &rfds); //将标准输入文件描述符加入 读集合
FD_SET(listenfd, &rfds); //将监听套结字 加入读集合
int max = listenfd +1; // max 值为最大文件描述符 + 1
while(1)
{
fds = rfds; //读集合
ret = select(max, &fds, NULL, NULL, NULL);
if(-1 == ret)
{
perror("select");
break;
}
for(i =0; i<max; i++) //因为要判断 0 所以从0 开始,如果只判断通信套结字就从listenfd +1 开始;
{
if(FD_ISSET(i, &fds)) //fds -》rfds ;
{
if(0 == i ) //标准输入准备就绪
{
memset(buf, 0, sizeof(buf));
fgets(buf, SIZE -1, stdin );
fputs(buf, stdout); //虽然stdout 没有加入特定集合,但是系统默认开启
}
if(listenfd == i) //监听套结字准备就绪
{
//正常通信模块
struct sockaddr_in caddr ;
memset(&caddr, 0, sizeof(caddr));
socklen_t clen = sizeof(caddr);
connfd = accept(listenfd,(struct sockaddr*)&caddr, &clen);
if(-1 == connfd)
{
perror("accept");
break;
}
printf("client: fd=%d:(%s:%d)\n",connfd, inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
//将新的通信套结字加入 读集合
FD_SET(connfd, &rfds);
max = (max > connfd)?max:(connfd +1);
}
else
{ //connfd 准备就绪
memset(buf, 0 ,sizeof(buf));
ret = recv(i, buf, sizeof(buf), 0); //类似read(i,buf, sizeof(buf));
if(-1 == ret)
{
perror("read");
break;
}
else if(0 == ret)
{
printf("client quit!\n ");
FD_CLR(i, &rfds); //将connfd 从rfds集合删除
close(i);
break;
}
else
{
fputs(buf, stdout);
}
}
}
}
}
return 0;
}
poll
poll和select基本是一样的,但是它对fd集合做了优化,使用链表存储,解决了连接数上限的问题。
tcp_poll_client.c
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
int main()
{
/********************************************
AF_INET IPv4 Internet protocols
SOCK_STREAM string socket
0
*********************************************/
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == sockfd)
{
perror("socket");
return -1;
}
printf("sockfd=%d\n",sockfd);
/*************************************************************
Internet协议地址结构�?/usr/include/netinet/in.h�?
struct sockaddr_in
{
u_short sin_family; // 地址�? AF_INET�? bytes
u_short sin_port; // 端口�? bytes
struct in_addr sin_addr; // IPV4地址�? bytes
char sin_zero[8]; // 8 bytes unused,作为填�?
};
struct in_addr{
in_addr_t s_addr; // u32 network address
};
****************************************************************/
struct sockaddr_in saddr;//定义Internet地址结构变量,保存服务器的ip及port
memset(&saddr,0,sizeof(saddr));//bzero
saddr.sin_family = AF_INET;//指定网络协议 IPV4
saddr.sin_port = htons(8000);//指定服务器的端口�?>=5001,由主机序转网络字节序
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//指定服务器的ip地址,ip地址由点分式�?2为无符号网络字节�?
socklen_t slen = sizeof(saddr);
int ret = connect(sockfd, (struct sockaddr *)&saddr,slen);//将服务器的ip和port与sockfd绑定
if(-1 == ret)
{
perror("connect");
goto ERR_STEMP;
}
printf("connect success\n");
#define SIZE 128
char buf[SIZE];
//使用IO多路复用解决阻塞问题
struct pollfd pollfd[1024];
memset(pollfd,0,sizeof(pollfd));
pollfd[0].fd = 0;
pollfd[0].events = POLLIN;
pollfd[1].fd = sockfd;
pollfd[1].events = POLLIN;
int count = 2;
do
{
ret = poll(pollfd,count,-1);
if(-1 == ret)
{
perror("poll");
break;
}
if(pollfd[0].revents == POLLIN)
{
memset(buf,0,SIZE);
fgets(buf,SIZE-1,stdin);//读从标准输入
ret = write(sockfd,buf,sizeof(buf));//给服务器写消�? if(-1 == ret)
{
perror("write");
break;
}
}
if(pollfd[1].revents == POLLIN)
{
memset(buf,0,SIZE);
ret = read(sockfd,buf,SIZE);//读服务器发送的数据
if(-1 == ret)
{
perror("read");
break;
}
else if(0 == ret)//服务器关�? {
printf("server closed\n");
close(pollfd[1].fd);
pollfd[1].fd = -1;
break;
}
else
fputs(buf,stdout);
}
}while(strncmp(buf,"quit",4) != 0);
close(sockfd);
return 0;
ERR_STEMP:
close(sockfd);
return -1;
}
tcp_poll_server.c
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
int server_init(char *ipaddr, unsigned short port,int backlog)//初始化服务器
{
/********************************************
AF_INET IPv4 Internet protocols
SOCK_STREAM string socket
0
*********************************************/
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == sockfd)
{
perror("socket");
return -1;
}
printf("sockfd=%d\n",sockfd);
/*************************************************************
Internet协议地址结构“ /usr/include/netinet/in.h”
struct sockaddr_in
{
u_short sin_family; // 地址族, AF_INET,2 bytes
u_short sin_port; // 端口,2 bytes
struct in_addr sin_addr; // IPV4地址,4 bytes
char sin_zero[8]; // 8 bytes unused,作为填充
};
struct in_addr{
in_addr_t s_addr; // u32 network address
};
****************************************************************/
struct sockaddr_in saddr;//定义Internet地址结构变量,保存服务器的ip及port
memset(&saddr,0,sizeof(saddr));//bzero
saddr.sin_family = AF_INET;//指定网络协议 IPV4
saddr.sin_port = htons(port);//指定服务器的端口号 >=5001,由主机序转网络字节序
//INADDR_ANY:任意ip地址
saddr.sin_addr.s_addr = (NULL == ipaddr)?(htonl(INADDR_ANY)):inet_addr(ipaddr);//指定服务器的ip地址,ip地址由点分式转32为无符号网络字节序
socklen_t slen = sizeof(saddr);
int ret = bind(sockfd, (struct sockaddr *)&saddr,slen);//将服务器的ip和port与sockfd绑定
if(-1 == ret)
{
perror("bind");
goto ERR_STEMP;
}
printf("bind success\n");
ret = listen(sockfd,backlog);//sockfd变为监听套接字
if(-1 == ret)
{
perror("listen");
goto ERR_STEMP;
}
return sockfd;
ERR_STEMP:
close(sockfd);
return -1;
}
int main()
{
int ret;
int sockfd = server_init(NULL, 8000,1024);//初始化服务器
if(-1 == sockfd)
{
printf("server_init error\n");
return -1;
}
printf("listen....\n");
struct sockaddr_in caddr;//保存客户端的ip及port
memset(&caddr,0,sizeof(caddr));
socklen_t clen = sizeof(caddr);
#if 0
//int rws = accept(sockfd,NULL, NULL);//rws用于和客户端通信
#else
int rws = accept(sockfd,(struct sockaddr *)&caddr, &clen);//rws用于和客户端通信
if(-1 == rws)
{
perror("accept");
close(sockfd);
return -1;
}
#endif
printf("rws=%d\n",rws);
#define SIZE 128
char buf[SIZE];
//使用IO多路复用解决阻塞问题
struct pollfd pollfd[1024];
memset(pollfd,0,sizeof(pollfd));
pollfd[0].fd = 0;//将标准输入添加到集合
pollfd[0].events = POLLIN;//指定文件描述符的读事件
pollfd[1].fd = rws;//将rws添加到集合
pollfd[1].events = POLLIN;
int count = 2;//指定监测的文件描述符的个数
while(1)
{
ret = poll(pollfd,count,-1);
if(-1 == ret)
{
perror("poll");
break;
}
if(pollfd[0].revents == POLLIN)//判断标准输入文件描述符是否准备就绪
{
memset(buf,0,sizeof(buf));
fgets(buf,SIZE-1,stdin);//读标准输入
ret = send(rws,buf,sizeof(buf),0);//给客户端发送消息
if(-1 == ret)
{
perror("write");
break;
}
}
if(pollfd[1].revents == POLLIN)//判断rws是否准备就绪
{
memset(buf,0,SIZE);
ret = recv(rws,buf,sizeof(buf),0);//读取客户端发送的消息
if(-1 == ret)
{
perror("read");
break;
}
else if(0 == ret)//客户端关闭
{
printf("client closed\n");
close(pollfd[1].fd);
pollfd[1].fd = -1;//文件描述符指定为-1时,表示当前文件描述符无效
break;
}
else
fputs(buf,stdout);
}
}
close(sockfd);
return 0;
}
epoll
epoll的实现与上述两种方式完全不同,因此不会造成上述的问题。
同select、poll不同,复杂度O(1),通过三个函数实现流程:
1.epoll_create: 创建一个epoll文件描述符集合,同时底层创建一个红黑树和就绪链表,红黑树存储所监控的文件描述符的节点数据,就绪链表存储就绪的文件描述符的节点数据。
2.epoll_ctl: 用于添加新的描述符,首先判断红黑树中是否存在,如果不存在,插入数据,并告知内核注册回调函数(当文件描述就绪时通过网卡驱动触发),数据就绪后将事件添加到就绪队列中。
3.epoll_wait: 检查链表,并将数据拷贝到用户空间(两者维护的是片共享内存),最后清空链表。其中epoll的工作方式分为LT、ET。
注意:epoll是线程安全的,但是当一个线程调用epollwait,而另一个线程用epollctl向同一个epoll_fd添加一个监测fd后,epollwait有可能被改fd的读/写事件唤醒。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <sys/epoll.h>
int main()
{
int listenfd;
int connfd;
int ret;
int i;
int j;
char buf[128];
struct sockaddr_in srvaddr;
struct sockaddr_in cltaddr;
socklen_t addrlen;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == listenfd) {
perror("socket");
return -1;
}
printf("create socket success !\n");
memset(&srvaddr, 0, sizeof(srvaddr));
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(8888);
#if 0
inet_aton("192.168.5.135", &(srvaddr.sin_addr));
#else
srvaddr.sin_addr.s_addr = inet_addr("0.0.0.0");
#endif
ret = bind(listenfd, (const struct sockaddr *)&srvaddr, sizeof(srvaddr));
if (-1 == ret) {
perror("bind");
return -1;
}
printf("srv bind success !\n");
ret = listen(listenfd, 1024);
if (-1 == ret) {
perror("listen");
return -1;
}
printf("listen success\n");
int epfd;
/* 创建一个epoll实例 */
epfd = epoll_create(1024); //1024表示的是某一个时间点期望检测到的最大个数;
if (-1 == epfd) {
perror("epoll_create");
return -1;
}
/* 将所要关心的文件描述符listenfd添加到eoll实例中 */
struct epoll_event event; //需要添加的事件和文件描述符;
event.events = EPOLLIN; //事件
event.data.fd = listenfd; //文件描述符
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event);
if (-1 == ret ) {
perror("epoll_ctl");
return -1;
}
struct epoll_event events[1024]; //用来存储检测到的事件和文件描述符的集合(小于等于epoll实例所能处理最大数)
while(1) {
ret = epoll_wait(epfd, events, 1024, 5000); //循环检测;
if (ret == -1) {
perror("epoll");
return -1;
}
#if 1
else if (0 == ret) {
printf("epoll timeout ....\n");
continue;
}
#endif
for (i = 0; i < ret; i++) { //对准备就绪的事件和文件描述符进行一一轮询;
int fd = events[i].data.fd;
if (events[i].events == EPOLLIN) { //判断是否为读事件
if (fd == listenfd) { //判断是否为监听套接字,如果是监听套接字则建立新的通信连接;
addrlen = sizeof(struct sockaddr_in);
connfd = accept(listenfd, (struct sockaddr*)&cltaddr, &addrlen);
if (-1 == connfd) {
perror("accept");
return -1;
}
printf("IP : %s -- PORT : %d\n", inet_ntoa(cltaddr.sin_addr) , ntohs(cltaddr.sin_port));
printf("accept connfd = %d success \n", connfd);
/* 连接成功则将通信套接字添加到epoll实例中 */
event.events = EPOLLIN;
event.data.fd = connfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event);
if (-1 == ret ) {
perror("epoll_ctl");
return -1;
}
} else { //如果是通信套接字,则处理客户端的数据请求。
memset(buf, 0, sizeof(buf));
ret = read(fd, buf, sizeof(buf));
if (-1 == ret) {
perror("read");
return -1;
} else if (0 == ret) {
printf("client quit\n");
/* 将退出的套接字文件描述符从epoll实例中清除 */
event.events = EPOLLIN;
event.data.fd = fd;
ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
if (-1 == ret ) {
perror("epoll_ctl");
return -1;
}
close(fd);
continue;
}
printf("buf : %s\n", buf);
}
} //if (fds[i].revents == POLLIN) {
} //for (i = 0; i < 1024; i++)
} //while(1)
close(listenfd);
return 0;
}
六、信号驱动IO模型
首先应用进程通过sigaction系统调用安装一个信号处理函数(在内核位置),该系统调用立即返回,进程继续工作(未被阻塞)。当数据准备就绪时,kernel就会为该进程产生一个SIGIO信号,随后用户进程就可以使用recvfrom调用去读取数据到内存,并返回成功指示。