,51LINUX应用编程和网络编程之六Linux高级IO

3.6.1.非阻塞IO

3.6.1.1、阻塞与非阻塞

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

补充:阻塞/非阻塞, 它们是程序在等待消息(无所谓同步或者异步)时的状态.

阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。

有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。

对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。

非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

3.6.1.2、为什么有阻塞式

(1)常见的阻塞:wait(显式回收子进程)、pause、sleep等默认阻塞函数;read或write某些文件时也是默认阻塞式的

(2)阻塞式的好处:当前线程被挂起等待条件满足才返回结果

3.6.1.3、非阻塞

(1)为什么要实现非阻塞

(2)如何实现【非阻塞IO访问】:

1)打开文件时加入O_NONBLOCK

2)fcntl函数,对文件描述符(文件已经打开)进行操作

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

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

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

3.6.2.阻塞式IO的困境

3.6.2.1、程序中读取键盘

3.6.2.2、程序中读取鼠标 cat /dev/input/mouse1

3.6.2.3、程序中同时读取键盘和鼠标

3.6.2.4、问题分析

并不是所有的情况下都适阻塞式io的。

代码示例:

#include <stdio.h>

#include <unistd.h>

#include <fcntl.h>

#include <sys/types.h>

#include <sys/stat.h>

/*

程序中读取键盘和鼠标

*/

#define NAME "/dev/input/mouse1"

char buff[100]={0};

char buf[100]={0};

int main(int argc,char **argv)

{

int ssize_t=-1;

int fd=-1;

fd=open(NAME,O_RDWR);

if(-1==fd)

{

perror("open");

_exit(-1);

}

printf("打开成功!fd=%d\n",fd);

while(1){

ssize_t=read(fd,buff,sizeof(buff));

if(-1==ssize_t)

{

perror("read");

_exit(-1);

}

printf("读到的鼠标字符数为%d\n",ssize_t);

printf("读到的鼠标字符是[%s]\n",buff);

read(0,&buf,sizeof(buf));

printf("读到的鼠标字符是[%s]\n",buf);

sleep(1);

}

return 0;

}

3.6.3.并发式IO的3种解决方案【并发式IO】

3.6.3.1、非阻塞式IO:性能不够好,有点类似于轮询的方式,CPU不停的去查看

代码示例:

#include <stdio.h>

#include <unistd.h>

#include <string.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#define NAME "/dev/input/mouse1"

char buff[100]={0}; //键盘

char buf[100]={0};//鼠标

int main(void)

{

int ssize_t=-1;

int fd=-1;

int flag=-1;

int ret=-1;

fd=open(NAME,O_RDWR | O_NONBLOCK );

if(-1==fd)

{

perror("open");

_exit(-1);

}

//把标准输入文件描述符0通过fcntl函数变成非阻塞式子

// 把0号文件描述符(stdin)变成非阻塞式的

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

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

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

// 这3步之后,0就变成了非阻塞式的了

while (1)

{

// 读鼠标

memset(buf, 0, sizeof(buf));

//printf("before 鼠标 read.\n");

ret = read(fd, buf, 50);

if (ret > 0)

{

printf("鼠标读出的内容是:[%s].\n", buf);

}

// 读键盘

memset(buff, 0, sizeof(buff));

//printf("before 键盘 read.\n");

ret = read(0, buff, 5);

if (ret > 0)

{

printf("键盘读出的内容是:[%s].\n", buff);

}

}

return 0;

}

3.6.3.2、多路复用IO :性能相对比较好,解决并发性IO的解决

3.6.4.IO多路复用原理

3.6.4.1、何为IO多路复用 【说白了,多路复用其实就是一个管多个】

(1)IO multiplexing

(2)用在什么地方?多路非阻塞式IO(多路及时响应)。

(3)select和poll两个函数:(poll出现的比较晚一点。其性能也要比select函数的性能更好一些)

(4)外部阻塞式(select和poll两个函数本身在调用的时候是阻塞式的,对外表现为阻塞式),内部非阻塞式(

两个函数内部实现的时候,也就是当把要监听的东西(比如A和B两个阻塞式IO)放在这个函数的监听范围)【自动】轮询【这两个多路阻塞式IO】 (轮询的意思就是CPU不停的去查看)

补充:

可是使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要

等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。

3.6.4.2、select函数介绍:

#include <sys/select.h>

#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); //nfds表示文件描述符的个数 nfds is the highest-numbered file descriptor in any of the //three sets, plus 1,是指集合中所有文件描述符的范围,【即所有文件描述符的最大值加1】这一点要注意,所有文件描述符的最大值加1,比如一个文件描述符是0,一个文件描述符是1,则为1+1

相关函数:

(1)void FD_ZERO(fd_set *set); //把文件描述符集合清零

(2)void FD_SET(int fd, fd_set *set); //把某个文件描述符添加到这个集合中去

(3)int FD_ISSET(int fd, fd_set *set);

//检查集合中指定的文件描述符是否可以读写 FD_ISSET() tests to see if a file descriptor is part of the set; this is useful after select() returns.

(4)void FD_CLR(int fd, fd_set *set); //把一个给定的文件描述符从集合中删除

相关结构体:

(1)struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor)。

(2)

struct timeval {

long tv_sec; /* seconds */

long tv_usec; /* microseconds */

};

struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

linux内部代码示例:

#include <stdio.h>

#include <stdlib.h>

#include <sys/time.h>

#include <sys/types.h>

#include <unistd.h>

int main(void)

{

fd_set rfds;

struct timeval tv;

int retval;

/* Watch stdin (fd 0) to see when it has input. */

FD_ZERO(&rfds); //先把文件描述符集合清零

FD_SET(0, &rfds); //把0文件描述符添加到这个集合中去

/* Wait up to five seconds. */

tv.tv_sec = 5;

tv.tv_usec = 0;

retval = select(1, &rfds, NULL, NULL, &tv);

/* Don't rely on the value of tv now! */

if (retval == -1)

perror("select()");

else if (retval)

printf("Data is available now.\n");

/* FD_ISSET(0, &rfds) will be true. */

else

printf("No data within five seconds.\n");

exit(EXIT_SUCCESS);

}

3.6.4.3、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 */

};

代码示例:

#include <stdio.h>

#include <unistd.h>

#include <string.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <poll.h>

/*

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

*/

#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);

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;

}

3.6.5.IO多路复用实践

3.6.5.1、用select函数实现同时读取键盘鼠标

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

(1)清空描述符集合

(2)建立需要监视的描述符与描述符集合的关系

(3)调用select函数

(4)检查监视的描述符判断是否已经准备好

(5)对已经准备好的描述符进程IO操作

代码示例:

/*

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

*/

#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);

printf("fd=%d\n",fd);

if(-1==fd)

{

perror("open");

_exit(-1);

}

FD_ZERO(&readfds); //清除文件描述符集合

FD_SET(fd,&readfds);

FD_SET(0,&readfds);

time.tv_sec=5; //设置时间为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

{

// 等到了一路IO,然后去监测到底是哪个IO到了,处理之

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;

}

3.6.5.2、用poll函数实现同时读取键盘鼠标

poll函数介绍:

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

第一个参数 pollfd 结构体定义如下:

引用

/* Data structure describing a polling request. */

struct pollfd

{

int fd; /* poll 的文件描述符. */

short int events; /* fd 上感兴趣的事件(等待的事件或者说是监视的事件). */

short int revents; /* fd 上实际发生的事件. */

};

fd 成员表示感兴趣的,且打开了的文件描述符;

events 成员是位掩码,用于指定针对这个文件描述符感兴趣的事件;

revents 成员是位掩码,用于指定当 poll 返回时,在该文件描述符上已经发生了哪些事情。

events 和 revents 结合下列常数值(宏)指定即将唤醒的事件或调查已结束的 poll() 函数被唤醒的原因,这些宏常数如下:

POLLIN

events 中使用该宏常数,能够在折本文件的可读情况下,结束 poll() 函数。相反,revents 上使用该宏常数,在检查 poll() 函数结束后,可依此判断设备文件是否处于可读状态(即使消息长度是 0)。

POLLPRI

在 events 域中使用该宏常数,能够在设备文件的高优先级数据读取状态下,结束 poll() 函数。相反,revents 上使用该宏常数,在检查 poll() 函数结束后,可依此判断设备文件是否处于可读高优先级数据的状态(即使消息长度是 0)。该宏常数用于处理网络信息包(packet) 的数据传递。

POLLOUT

在 events 域中使用该宏常数,能够在设备文件的写入状态下,结束 poll() 函数。相反,revents 域上使用该宏常数,在检查 poll() 结束后,可依此判断设备文件是否处于可写状态。

POLLERR

在 events 域中使用该宏常数,能够在设备文件上发生错误时,结束 poll() 函数。相反,revents 域上使用该宏函数,在检查 poll() 函数结束后,可依此判断设备文件是否出错。

POLLHUP

在 events 域中使用该宏常数,能够在设备文件中发生 hungup 时,结束 poll() 函数 。相反,在检查 poll() 结束后,可依此判断设备文件是否发生 hungup 。

POLLNVAL

在 events 域中使用该宏函数,能够在文件描述符的值无效时,结束 poll() 。相反,在 revents 域上使用该宏函数时,在检查 poll() 函数后,文件描述符是否有效。可用于处理网络信息时,检查 socket handler 是否已经无效。

最后一个参数 timeout 指定 poll() 将在超时前等待一个事件多长事件。这里有 3 种情况:

1) timeout 为 -1

这会造成 poll 永远等待。poll() 只有在一个描述符就绪时返回,或者在调用进程捕捉到信号时返回(在这里,poll 返回 -1),并且设置 errno 值为 EINTR 。-1 可以用宏定义常量 INFTIM 来代替(在 pth.h 中有定义) 。

2) timeout 等于0

在这种情况下,测试所有的描述符,并且 poll() 立刻返回。这允许在 poll 中没有阻塞的情况下找出多个文件描述符的状态。

3) time > 0

这将以毫秒为单位指定 timeout 的超时周期。poll() 只有在超时到期时返回,除非一个描述符变为就绪,在这种情况下,它立刻返回。如果超时周期到齐,poll() 返回 0。这里也可能会因为某个信号而中断该等待。

和 select 一样,文件描述符是否阻塞对 poll 是否阻塞没有任何影响。

代码示例:

#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);

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;

}

3.6.6.异步IO

3.6.6.1、何为异步IO

(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信号(类似于中断信号)从而执行绑定的处理函数去处理这个异步事件。

3.6.6.2、涉及的函数:

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

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

3.6.3.代码实践

3.6.7.存储映射IO

存储映射IO使一个磁盘文件(物理地址)与存储空间(内存地址)中的一个缓冲区相映射。于是当从缓冲区取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,则相应字节就自动地写入文件。这样就可以在不使用read和write的情况下执行IO。为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中,这是由mmap函数实现的。

3.6.7.1、mmap函数:把一个磁盘文件和一个内存映射起来 内存映射函数

3.6.7.2、LCD显示和IPC之共享内存

3.6.7.3、存储映射IO的特点

(1)共享而不是复制,减少内存操作

(2)处理大文件时效率高,小文件不划算(视频用到的也比较多)

函数原型:

void *mmap(void *addr, size_t length, int prot, int flags,

int fd, off_t offset);

addr参数用于指定映射存储区的起始地址,通常将其设置为0,这表示由系统选择该映射区的起始地址,此函数的返回地址是该映射区的起始地址。

fd指定要被映射文件的描述符,在映射该文件到一个地址空间之前,先要打开该文件。

length是映射的字节数。

offset是要映射字节在文件中的起始偏移量。

prot参数说明对映射存储区的保护要求,但不能超过文件open模式访问权限,prot可选值如下:

PROT_EXEC Pages may be executed.

PROT_READ Pages may be read.

PROT_WRITE Pages may be written.

PROT_NONE Pages may not be accessed.

flags参数影响映射存储区的多种属性,其中MAP_SHARED和MAP_PRIVATE两者必须选择其一,还有许多其它的MAP_XXX是可选的。

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