900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > linux应用编程和网络编程-3.6.高级IO 3种并发式IO:非阻塞式轮询+IO复用+异步IO /

linux应用编程和网络编程-3.6.高级IO 3种并发式IO:非阻塞式轮询+IO复用+异步IO /

时间:2024-03-08 02:15:20

相关推荐

linux应用编程和网络编程-3.6.高级IO 3种并发式IO:非阻塞式轮询+IO复用+异步IO  /

/kelamoyujuzhen/p/9455080.html

非阻塞IO

阻塞与非阻塞:

阻塞:阻塞具有很多优势(linux系统的默认设置),单路IO的时候使用阻塞式IO没有降低CPU的性能

·

阻塞式改成非阻塞式的2种方法:

①静态文件-----以O_NONBLOCK方式打开

②动态文件-----①fcntl函数提取出flags②flags位或O_NONBLOCK③更新flags

flag = fcntl(fd, F_GETFL); // 先获取原来的flag

flag |= O_NONBLOCK; // 添加非阻塞属性

fcntl(fd, F_SETFL, flag); // 更新flag

(F_SETEL只能改变O_APPEND,O_NONBLOCK和O_ASYNC,其他的不能改)

单路IO就用阻塞式比较好;

多路IO最好是用非阻塞式的。避免资源被一个占有不放,让其他IO有资源可抢。

从CPU的利用角度来看,单路IO就是一对一;多路IO就是一对多,即一个CPU对应多个资源抢占通道。前者单路效率更高,后者需要兼顾分配。单路IO模型只需要监听一个IO流,多路IO模型可以同时监听(内部轮循)多个IO流,CPU要不停去查看。

并发式IO的3种实现方案

①非阻塞式IO轮询:性能不够好,太占用CPU

while(1){调用非阻塞IO+调用非阻塞IO+调用非阻塞IO……}

②IO多路复用

③异步IO

非阻塞式IO轮询代码示例:

//要先保证:以非阻塞方式打开 or 为打开了的fd的flag设置为非阻塞👈fcntl函数实现while (1){memset(buf, 0, sizeof(buf));ret = read(fd1, buf, 50);if (ret > 0){printf("fd1读出的内容是:[%s].\n", buf);}memset(buff, 0, sizeof(buff));//printf("before 键盘 read.\n");ret = read(fd3, buff, 5);if (ret > 0){printf("fd2读出的内容是:[%s].\n", buff);}}

IO复用IO multiplexing【对外阻塞,对内非阻塞-限时等待】本质是同步I/O

I/O多路复用通过一种机制,可以监视多个描述符,一旦某个描述符就绪(读/写/执行就绪),能够通知程序进行相应的读写操作。

Linux API提供的I/O复用方式:【select poll epoll】

select,poll,epoll本质上都是同步I/O

因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的

异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

与多线程和多进程相比,I/O 多路复用的最大优势是系统开销小,系统不需要建立新的进程或者线程,也不必维护这些线程和进程。

IO复用使得程序可以同时监听多个文件描述符的就绪事件的发生,应用场景如:

1当需要同时处理多个描述符(通常是交互式输入、网络套接字)时,必须使用I/O多路复用;

2tcp服务器既要处理监听套接字,又要处理已连接套接字,一般要使用I/O多路复用;

3如果一个服务器既要处理tcp又要处理udp,一般要使用I/O多路复用;

4如果一个服务器要处理多个服务时,一般要使用I/O多路复用。

不过要清楚的一点是,IO复用虽然能同时监听多个文件描述符,但它是阻塞监听的,当有多个文件描述符同时就绪时,若不采取额外措施,程序只能按照顺序依次处理其中的每一个就绪事件。这实际上也是串行工作,要实现并发,只能使用多进程或多线程等编程手段。

select和poll函数对外表现为阻塞式,对内表现为非阻塞式(对内设定等待时长)

/* According to POSIX.1-2001 */#include <sys/select.h>/* According to earlier standards */#include <sys/time.h>#include <sys/types.h>#include <unistd.h>int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);void FD_CLR(int fd, fd_set *set);//清除某一个被监视的文件描述符。int FD_ISSET(int fd, fd_set *set);//测试一个文件描述符是否是集合中的一员void FD_SET(int fd, fd_set *set);//添加一个文件描述符,将set中的某一位设置成1;void FD_ZERO(fd_set *set);//清空集合中的文件描述符,将每一位都设置为0;

select()函数实现IO多路复用的步骤

设置并填充监听结构体fd_set1、fd_set readfds;//由很多个二进制构成的数组,每一位表示一个文件描述符是否需要监视。2、FD_ZERO(&readfds); //【初始化套接字集合,即所有为清零】3、FD_SET(fd,&readfds);//将fd添加进select函数监听结构体 对应位置一//FD_SET(0,&readfds);//将stdin添加进select函数监听fd结构体设置等待时间struct timeval time;超时时间结构体//struct timeval {// long tv_sec; /* seconds */秒// long tv_usec; /* microseconds */微秒 1000000微秒=1秒//};time.tv_sec=5; //设置时间(秒)为5秒time.tv_usec=0; //设置时间(微秒)为0微秒调用select函数int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);//int nfds =监听结构体中最大的fd+1(因为fd从0开始)//fd_set *readfds, fd_set *writefds, fd_set *exceptfds 将监听结构体放到对应位置 不感兴趣就设置成NULL三种情况:①设为阻塞:时间为NULL(不传入时间结构)②设为非阻塞:时间设为0秒0毫秒,立即返回③设置为限时阻塞:超时时间timeout的值大于0,超出时间就返回返回:0超时正数:就绪的fd数量-1出错并设置error{EBADF:集合中包含无效的文件描述符。(文件描述符已经关闭了,或者文件描述符上已经有错误了)。EINTR:捕获到一个信号。EINVAL:nfds是负的或者timeout中包含的值无效。ENOMEM:无法为内部表分配内存。}//比如一开始要监听125 且fd_set=0001_0011长度这里暂取1字节//setect发现12就绪 而5不就绪 则fd_set变为0000_0011并函数返回2//不就绪的5清零 就绪的12保持置位 那么stdin=0怎么表示 应该是最低一位把?判断哪个/哪些fd准备好了FD_ISSET(int fd, fd_set *fd_set) 为真表示就绪//FD_CLR(int, fd_set*) 对应位清零 移除fd

评价:良好的跨平台(支持几乎在所有平台上都支持),

缺点:

1、能够监视的fd数量存在上限,在linux上一般为1024(可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低)【内核定义了fd_set中1024为监听个数上限同时也是文件描述符上限,如果要扩大,只能重新编译内核。】

2、每次调用 select(),都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大,同时每次调用 select() 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大;

IO 多路复用之select(高效并发服务器) select并发IO和TCPIP协议共用

代码示例:

#include <stdio.h>#include <stdlib.h>#include <sys/time.h>#include <sys/types.h>#include <unistd.h>#define NAME"/dev/input/mouse1" char buf[100]={0};int main(void){int fd=-1;fd_set readfds=-1;struct timeval time;int ret=-1;fd=open(NAME,O_RDWR);if(-1==fd){perror("open"); _exit(-1); }FD_ZERO(&readfds); FD_SET(fd,&readfds);FD_SET(0,&readfds);//将stdin添加进select函数监听fd结构体time.tv_sec=5;time.tv_usec=0;ret=select(3,&readfds,NULL,NULL,&time); //调用select函数 ,注意第一个参数if (ret < 0) {perror("select: "); return -1;}else if (ret == 0) {printf("超时了\n");}else{if (FD_ISSET(0, &readfds)) {// 这里处理键盘memset(buf, 0, sizeof(buf));read(0, buf, 5);printf("键盘读出的内容是:[%s].\n", buf);}if (FD_ISSET(fd, &readfds)){// 这里处理鼠标memset(buf, 0, sizeof(buf));read(fd, buf, 50);printf("鼠标读出的内容是:[%s].\n", buf);}}return 0;}

poll

#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);#define _GNU_SOURCE /* See feature_test_macros(7) */结构体:struct pollfd {int fd; /* file descriptor */short events;/* requested events *///感兴趣(等待的/监视)的事情short revents; /* returned events *///fd上实际发生的事情(这2个【event的值】用特定宏表述)};

pollfd结构包含了要监视的event和实际发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)

select和poll都需要在返回后,通过【遍历文件描述符】来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态。

poll流程

1、初始化并填充监听结构体数组struct pollfd a[?]={0}; a[0].fd=fd;//鼠标a[0].events=POLLIN;a[1].fd=0; //键盘a[1].events=POLLIN;2、②调用poll函数int ret=poll(a,fd+1,10000); //等待时间不再使用结构体,而是使用int3、查看返回值是否>04、查看特定fd是否预期和实际时间一致if(a[?].events==a[?].revents)5、对已经准备好的描述符进程IO操作

#include <stdio.h>#include <unistd.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <poll.h>#define NAME "/dev/input/mouse1"int main(void){char buf[100]={0}; //注意这里的定义的buf不能够使用全局的//int poll(struct pollfd *fds, nfds_t nfds, int timeout);struct pollfd a[2]={0};int fd= open(NAME,O_RDWR); //定义鼠标文件描述符if(fd<0){perror("open mouse");_exit(-1);}//实例化结构体a[0].fd=fd;//鼠标a[0].events=POLLIN;a[1].fd=0; //键盘a[1].events=POLLIN;int ret=poll(a,fd+1,10000);if(ret<0){perror("poll");_exit(-1);} if(ret==0){printf("超时了");}else//判断是谁发生了IO{if(a[0].events==a[0].revents){//鼠标memset(buf,0,sizeof(buf));read(fd,buf,10);//read本身是阻塞函数,但这里一定满足条件,不会发生阻塞,会直接读取到内容printf("读出来的鼠标内容是:[%s]\n",buf);}if(a[1].events==a[1].revents){//键盘memset(buf,0,sizeof(buf));read(0,buf,50);printf("读出来的键盘内容是:[%s]\n",buf);}} return 0;}

epoll

int epoll_create(int size);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *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 */

};

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

主要是epoll_create,epoll_ctl和epoll_wait三个函数。epoll_create函数创建epoll文件描述符,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。返回是epoll描述符。-1表示创建失败。epoll_ctl 控制对指定描述符fd执行op操作,event是与fd关联的监听事件。op操作有三种:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。epoll_wait 等待epfd上的io事件,最多返回maxevents个事件。

在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。

四.epoll的优点主要是一下几个方面

监视的描述符【数量不受限制】,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048。select的最大缺点就是进程打开的fd是有数量限制的(1024,也可以修改宏扩增)。

IO的【效率不会】随着监视fd的数量的增长而【下降】。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。

支持【电平触发和边沿触发】(只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发)两种方式,理论上边缘触发的性能要更高一些,但是代码实现相当复杂。

mmap加速内核与用户空间的信息传递。epoll是通过内核于用户空间mmap同一块内存,【避免了无谓的内存拷贝】。

异步IO

原理:条件就绪【比如:底层把数据准备好】后,内核就会给进程发送一个“异步通知的信号”通知进程,表示数据准备好了,然后进程调用信号处理函数去读数据(异步通知信号捕获函数需要用户自己定义),在没有准备好时,进程忙自己的事情

使用异步IO有两个前提,(1)底层驱动必须要能够发送SIGIO//驱动自带or自己写(2)应用层必须进行相应的异步IO的设置,否者无法使用异步IO 应用层进行异步IO设置时,设置使用的也是fcntl函数

(1)几乎可以认为:异步IO就是操作系统用软件实现的一套中断响应系统(有点类似与硬件中断)。

两种类型的文件IO同步:同步文件IO和异步文件IO。异步文件IO也就是重叠IO。【在同步文件IO中,线程启动一个IO操作然后就立即进入等待状态,直到IO操作完成后才醒来继续执行。而异步文件IO方式中,线程发送一个IO请求到内核,然后继续处理其他的事情,内核完成IO请求后,将会通知线程IO操作完成了。

如果IO请求需要大量时间执行的话,异步文件IO方式可以显著提高效率,因为在线程等待的这段时间内,CPU将会调度其他线程进行执行,如果没有其他线程需要执行的话,这段时间将会浪费掉(可能会调度操作系统的零页线程)。如果IO请求操作很快,用异步IO方式反而还低效,还不如用同步IO方式。

同步IO在同一时刻只允许一个IO操作,也就是说对于同一个文件句柄的IO操作是序列化的,即使使用两个线程也不能同时对同一个文件句柄同时发出读写操作。重叠IO允许一个或多个线程同时发出IO请求。

异步IO在请求完成时,通过将文件句柄设为有信号状态来通知应用程序,或者应用程序通过GetOverlappedResult察看IO请求是否完成,也可以通过一个事件对象来通知应用程序。

简单的说“同步在编程里,一般是指某个IO操作执行完后,才可以执行后面的操作。异步则是,将某个操作给系统,主线程去忙别的事情,等内核完成操作后通知主线程异步操作已经完成。”

(2)异步IO的工作方法是:我们当前进程向内核注册一个异步IO事件(使用signal注册一个信号SIGIO的处理函数),然后当前进程可以正常处理自己的事情,内核就去帮你完成或者说是检测你希望的事件是否发生,当异步事件发生后当前进程会收到内核传来的一个SIGIO信号(类似于中断信号)从而执行绑定的处理函数去处理这个异步事件。

涉及的函数:

(1)fcntl(F_GETFL、F_SETFL、O_ASYNC、F_SETOWN) 设置异步通知

(2)signal或者sigaction函数(SIGIO信号)

使用异步IO方式读鼠标和键盘

进程正常阻塞读键盘,然后将读鼠标设置为异步IO方式。

进程正常阻塞读键盘时,如果鼠标没有数据的话,进程不关心读鼠标的事情,如果鼠标数据来了,底层鼠标驱动就会向进程发送一个SIGIO信号,然后调用注册的SIGIO信号捕获函数读鼠标数据。

当然也可以反过来,进程正常阻塞读鼠标,然后将读键盘设置为异步IO方式。

异步IO流程

①设置fd使其支持异步IO

flag = fcntl(fd, F_GETFL);flag |= O_ASYNC; //设置为可以接受异步IO信号fcntl(fd, F_SETFL, flag);//设置fd的异步IO事件的接收进程设置为当前进程getpid() 否则底层驱动并不知道将SIGIO信号发送给哪一个进程fcntl(fd, F_SETOWN,getpid()); //getpid()返回值为当前进程//信号发送者 信号接受者//准备好捕获函数+注册当前进程的SIGIO信号捕获函数 void func(int sig){……} //(捕获信号后的)处理函数// 注册当前进程的SIGIO信号捕获函数 signal ( SIGIO , void ( * getmyinput ) ( int signum ) );signal(SIGIO, func);

④主函数就做其他的事情,异步IO事件就交给异步IO通知和捕获函数处理

代码实践

#include <stdio.h>#include <unistd.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <signal.h>int mousefd = -1;// 若接受到SIGIO信号,则处理异步通知事件void func(int sig){char buf[200] = {0};if (sig != SIGIO)//信号 SIGIO/SIGPOLL=8 指示一个异步IO事件return;read(mousefd, buf, 50);printf("鼠标读出的内容是:[%s].\n", buf);}void print_err(char *str,int line,int err_no)//鼠标设备文件打开失败报错打印{printf("%d,%s:%s\n",line,str,strerror(err_no) );//打印:错误的行,open mouse0 fail:strerror(errno)exit(-1);}int main(void){// 读取鼠标char buf[200];int flag = -1;mousefd = open("/dev/input/mouse1", O_RDONLY);if(mousefd == -1) print_err("open mouse0 fail",__LINE__,errno);//打开鼠标文件失败 报错// 把鼠标的文件描述符设置为可以接受异步IOflag = fcntl(mousefd, F_GETFL);flag |= O_ASYNC;fcntl(mousefd, F_SETFL, flag);//当输入缓存中的输入数据就绪时(输入数据可读),内核向用F_SETOWN来绑定的那个进程发送SIGIO信号。fcntl(mousefd, F_SETOWN, getpid());signal(SIGIO, func);// 读键盘,进程正常阻塞读键盘,然后将读鼠标设置为异步IO方式。while (1){memset(buf, 0, sizeof(buf));//printf("before 键盘 read.\n");read(0, buf, 5);printf("键盘读出的内容是:[%s].\n", buf);}return 0;}

【还有一种就是多进程下,父进程fork创建一个子进程来处理不同的事情】。

linux应用编程和网络编程-3.6.高级IO 3种并发式IO:非阻塞式轮询+IO复用+异步IO //存储映射IO(即mmap) select

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。