一,阻塞式IO
当需要执行一项任务,而且去执行这项任务必须要满足一个条件,如果在条件满足前一直等待,直到满足条件再去完成任务。
这种方式就是阻塞式 。
比如:一个人去钓鱼,没有鱼上钩就一直等待,直到有鱼上钩,下去拉鱼竿。
二,非阻塞式IO
当需要执行一项任务,而且去执行这项任务必须要满足一个条件,如果在条件满足前不等待 ,而是去干其他的事情,但要按时查看条件是否满足,直到满足条件再去完成任务。
这种方式就是非阻塞式 。
比如:一个人去钓鱼,没有鱼上钩就去睡觉,每5分钟查看 一次,直到有鱼上钩,下去拉鱼竿。
三,信号驱动IO(缺点太多)
当需要执行一项任务,而且去执行这项任务必须要满足一个条件,如果在条件满足前不等待 ,而是设置一个报告,如果有报告发生,必须马上去完成任务。
这种方式就是信号驱动式 。
比如:一个人去钓鱼,没有鱼上钩就去睡觉,在鱼竿上安装一个门铃,直到有鱼上钩触发闹铃,下去拉鱼竿。
四,IO多路转换
比如:一个人去钓鱼,但是他拿了5个或者更多的鱼竿,在没有鱼上钩的时候,就等着,有鱼上钩就拉杆。
其实就是多个阻塞式任务一起执行。
五,异步IO
比如:一个人去钓鱼,没有鱼上钩就去做其他的事,雇佣一个人去给我看着鱼竿,有鱼上竿了,并且给我拉上来了,给我发送一个消息,我不一定马上去收鱼 ,可以等我有时间在去。
总结:
任何IO过程中 , 都包含两个步骤. 第⼀是等待, 第⼆是拷⻉. ⽽且在实际的应⽤场景中, 等待消耗的时 间往往都远远⾼于拷⻉的时间. 让IO更⾼效, 最核⼼的办法就是让等待的时间尽量少.。
六,阻塞 VS 非阻塞
阻塞与非阻塞关注的是程序在等待调用结果返回前的状态。
阻塞调用是指在等待调用结果是时,当前线程会被挂起,直到调用 结果返回。
⾮阻塞调⽤指在不能⽴刻得到结果之前,该调⽤不会阻塞当前线程.
七,同步通信 VS 异步通信
同步:调用者发出一个调用,在没有得到结果之前,该调⽤就不返回. 但是⼀旦调⽤返回, 就得到返回值了; 换句话说,就是由调⽤者主动等待这个调⽤的结果。
异步:异步调用则相反,调用者发出调用后,直接返回了,不主动的等待调用的结果,而之后调用会在产生结果后通知调用者。
同步与互斥:这里的同步指的为了完成一个任务,多个进程和多个线程按照一定的顺序执行。互斥指的是多个线程访问临界资源时相互排斥,比如,一个进程向文件里读,另一个进程不能系统向文件里写
总结:
举例:给小孩换尿布
同步阻塞:主动等待返回结果,而且线程挂起,比如:守在小孩身边,不做其他的事,一直等着小孩哭,小孩哭了就去换尿布。
异步阻塞:被动等待结恶,而且线程挂起,比如:守在小孩身边,不做其他的事,但是不等待小孩哭,小孩哭了就去换尿布。
同步非阻塞:
主动等待返回结果,但是线程不挂起,比如:不守在小孩身边,做其他的事,但一直等着小孩哭,小孩哭了就去换尿布。
异步非阻塞:被动等待结恶,而且线程不挂起,比如:不守在小孩身边,做其他的事,但是不等待小孩哭,小孩哭了就去换尿布。
八,io多路转接之select
1,什么是select?
系统提供select函数来实现多路复用输出/输入模型
1,select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的:
2,程序会停在select这里等待,直到被监视的文件描述符右一个或多个发生了变化;
2,select函数原型
#include<sys/select.h>
int select (int nfds , fd_set * readfds ,fd_set* writefds ,fd_set * exceptfds ,struct timeval * timeout);
1,int nfds :这个参数是需要监视的最大的文件描述符+1;
2,rdset ,wrset , exset分别是需要检测的可读⽂件描述符的集合,可写⽂件描述符的集合及异常⽂件描述符的集合;
这三个都是fd_set结构体指针:这个结构体内部就是一个整数(long)的数组,将他用作位图使用,也就是这个数组中的每一 个比特位表示一个文件描述符的状态,第10个比特位就表示了10号文件描述符的状态。
下面提供一组函数控制位图
void FD_SET(int fd, fd_set *fdset); //将fd加入set集合
void FD_CLR(int fd, fd_set *fdset); //将fd从set集合中清除
int FD_ISSET(int fd, fd_set *fdset); //检测fd是否在set集合中,不在则返回0
void FD_ZERO(fd_set *fdset); //将set清零使集合中不含任何fd
3,struct timeval * timeout :这个结构体的结构如下,用来设置select()的等待时间
ttimeout 的取值选择
1, NULL:则表⽰select()没有timeout,select将⼀直被阻塞,直到某个⽂件描述符上发⽣了事件;(无限等待)
,2, 0:仅检测描述符集合的状态,然后⽴即返回,并不等待外部事件的发⽣。 (等待时间就是0)
,3,特定的时间值:如果在指定的时间段⾥没有事件发⽣,select将超时返回。(设定等待时间)
//头文件
<sys/time.h>
//结构
timeval
{
time_t tv_sec; //秒 [long int]
suseconds_t tv_usec; //微秒 [long int]
};
4:函数的返回值
1,成功返回文件描述符改变的个数,
,2,返回0表示在文件描述符状态改变前已经超出等待时间,没有返回
,3,当有错误发⽣时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout 的值变成不可预测
3,理解select执行过程
为了方便说明,取fd_set这个位图的大小时一个字节,共用8个 比特位,可以监控的文件描述符是0~7。
1,fd_set set;int fd = 5,执行 FD_ZERO( &set ),set位图是 0000 0000。
2,执行 FD_SET ( fd,&set ),set位图是 0010 0000,再加入int fd=1,int fd=2;set位图的结果是 0010 0110
3,执⾏select(6,&set,0,0,0)阻塞等待 *(5)若fd=1,fd=2上都发⽣可读事件,则select返回,此时set 变为0000,0110。
,4,注意:没有事件发⽣的fd=5被清空
4,select的特点
1,可监控的文件描述符的个数取决于sizeof( fd_set )的值,sizeof(fd_set)的值是字节数的大小,
所以总共可监控的 描述符的值时sizeof(fd_set)*8个 ,当文件描述符太多时,select就搞不定了。
2,将fd 加入select返回后,需要用将被监控的 fd 集合保存一份,保存到tmp中
1,因为select返回后,没有状态就绪的文件描述符会被清空,导致下次监控不到,如listen_socket,s
所以保存下来,在进入下次select之前,重新设置。
2,用经过select处理的位图,去判断是加入新的文件描述符还是进行读写操作。
5,select的缺点
1, 重复的使用文件描述符集合使用户每次调用select时都要重新设置文件描述符集合 ,
需要用户自己重新定义输入的文件描述符表,和输出的文件描述表,从接口使用角度来说也不方便。
2,每次调用select,都需要把fd集合从用户 态拷贝到内核态,这个开销在fd很多时会很大
3,同时每次调⽤select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很⼤
4,select⽀持的⽂件描述符数量太⼩
6,socket就绪的条件
只要文件描述符的是就绪状态,那么这个文件描述符在被select函数监控后,就不会被清空
读就绪
1, socket内核中, 接收缓冲区中的字节数, ⼤于等于低⽔位标记SO_RCVLOWAT.
此时可以⽆阻塞的读 该⽂件描述符, 并且返回值⼤于0;
2,socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
3,监听的socket上有新的连接请求; socket上有未处理的错
写就绪
1,socket内核中, 发送缓冲区中的可⽤字节数(发送缓冲区的空闲位置⼤⼩),
⼤于等于低⽔位标记 SO_SNDLOWAT, 此时可以⽆阻塞的写, 并且返回值⼤于0;
2,socket的写操作被关闭(close或者shutdown). 对⼀个写操作被关闭的socket进⾏写操作, 会触发 SIGPIPE信号;
3,socket使⽤⾮阻塞connect连接成功或失败之后;
九,编写基于 TCP的select 多路复用网络服务器
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/select.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
typedef struct Fdset
{
fd_set set;
int max_fd; //set 之中的最大文件描述符是哪个
}FdSet;
void FdSetInit(FdSet* fdset)
{
FD_ZERO(&fdset->set);//初始化set
fdset->max_fd=0;
}
//此处的插入过程需要 维护max_fd
void FdSetAdd(FdSet* fdset,int fd)
{
FD_SET(fd,&fdset->set);
if(fd > fdset->max_fd)//将最大的文件描述符保存起来
{
fdset->max_fd=fd;
}
}
void FDSetDel(FdSet* fdset , int fd)
{
FD_CLR(fd,&fdset->set);
//如果删最大的文件描述符,就要更新最大文件描述符,那怎么去找第二大文件描述符
int max_fd=-1;
int i=0;
for( ;i<=fdset->max_fd;i++)
{
if(!__FD_ISSET(i,&fdset->set))
{
continue;
}
if(i>max_fd)
{
max_fd=i;
}
}
fdset->max_fd=max_fd;
}
int ProcessRequest(int new_sock)
{
//循环从 socket中读取数据
char buf[1024]={0};
ssize_t read_size = read(new_sock,buf,sizeof(buf)-1);
if(read_size <0 )
{
perror("read");
return -1;
}
if(read_size==0)
{
printf("read done");
return 0;
}
buf[read_size]='\0';
printf("[client %d]say :%s\n",new_sock,buf);;
write(new_sock,buf,sizeof(buf));
return 1;
}
int ServerInit(char* ip,short port)
{
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd<0)
{
perror("socket");
return -1;
}
sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=inet_addr(ip);
addr.sin_port=htons(port);
int ret=bind(fd,(sockaddr*)&addr,sizeof(addr));
if(ret<0)
{
perror("bind");
return -1;
}
ret=listen(fd,5);
if(ret<0)
{
perror("listen");
return -1;
}
return fd;
}
//./server [ip] [port]
int main (int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage ./server [ip] [port]\n");
return 1;
}
//服务器初始化,创建一个socket
int listen_socket=ServerInit(argv[1],atoi(argv[2]));
if(listen_socket<0)
{
printf("ServerInit Failed\n");
return 1;
}
printf("ServerInit KO!\n");
//2,把listen_socket 添加到select对应的文件描述符位图中
FdSet input_fds;
FdSetInit(&input_fds);
FdSetAdd(&input_fds,listen_socket);
while(1)
{
//input_fds 添加内容:有新的客户端连接进来
//input_fds 删除内容:客户端断开连接才删除
FdSet output_fds=input_fds;//因为一经过 select函数,位图就会发生变化,所以将位图先保存起来
int ret=select(output_fds.max_fd+1,&(output_fds.set),NULL,NULL,NULL);
if(ret<0)
{
perror("select");
continue;
}
if(FD_ISSET(listen_socket,&output_fds.set))//这里的要监控的位图有可能改变,所以用output-fds
{
//条件成立,就是有新的客户端件,建立连接,那么liste_socket就会读就绪
//此时就可以直接调用accept获取这个连接
sockaddr_in peer;
socklen_t len=sizeof(peer);
int new_sock = accept(listen_socket,(sockaddr*)&peer,&len);
if(new_sock<0)
{
perror("accept");
continue;
}
FdSetAdd(&input_fds,new_sock);//有新的文件描述符加入了,就必须更新到原始位图中
printf("[client %d]\n",new_sock);
}
else//不是listen_socket 那就是new_socket
{
int i=0;
for(;i<=output_fds.max_fd;i++)
{
if(!FD_ISSET(i,&output_fds.set))//这里的位图肯定会变化,所以用output_fds
{
continue;
}
//对当前就绪的文件描述符进行读写处理操作
int ret=ProcessRequest(i);
//在这里listen_socket就没有了,所以要解决这个bug
if(0 == ret)
{
//说明对端关闭了socket
close(i);
FDSetDel(&input_fds,i);//有文件描述符关闭了,就必须要更新到原始的位图中
printf("[cilent %d] cut line!",i);
}
}
}
}
return 0;
}
十,IO多路复用之-----poll
1,函数原型
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
2,参数分析
struct polled * fds :当只关注一个文件描述符时,就填一个结构体指针,当关注多个文件描述符时,就要传一个结构体数组指针。
在polled结构体中,int fd:表示要关注的文件描述符
evevts:输入参数,用一组宏中的一个来替换,表示要关注什么事,如读就绪
revents:输出参数,也是宏,比如读就绪是否等待完成。
nfds_t nfds: 关注的文件描述符的个数
int timout ; 等待的时间
3,编写例子
#include<stdio.h>
#include<unistd.h>
#include<poll.h>
int main ()
{
//定义一个这样的结构体就只能检测一个文件描述符
//要监控多个文件描述符就要定义一个结构体数组
struct pollfd fds;//这里只监控一个
fds.events=POLLIN;
while(1)
{
int ret=poll(&fds,1,0);
if(ret<0)
{
perror("poll");
}
//如果poll返回了,就说明0号文件描述符就绪了
char buf[1024]={0};
ssize_t read_size=read(0,buf,sizeof(buf)-1);
if(read_size<0)
{
perror("read");
return 1;
}
if(read_size==0)
{
printf("read done!");
return 0;
}
buf[read_size]='\0';
printf ("%s\n",buf);
}
return 0;
}
十一;poll和select的比较
1,在接口方面,poll和select每次都需要重新设置文件描述符。但是poll设置了输出文件描述符集合,
相当于解决了select要用户自己定义输出描述符表的问题。不存在描述符表复用的问题。
2,select需要将fd位图集合拷贝到内核,poll也需要将结构体数组(多个文件描述符),
拷贝到内核,而且是结构体,开销更大。拷贝的频率也是一样的。
3,select和poll也都需要遍历文件描述符集合。
4,select有上限,是4096,poll没有上限,可以有无限制个结构体。