IO进程(Linux IO模型)

前言

思维导图

思考

试想一下三种情况:你想打游戏,但是怕爸妈回来

1.一直趴在猫眼看,但是你玩不了游戏:你为了确定爸妈是否回来,阻塞了自己打游戏的进程。

2.时不时从猫眼看一眼,你可以打游戏,但是比较累:你要在观察爸妈是否回来(看猫眼)的间隔打游戏。

3.你用耳朵去听,传来开门的声音,就停止打游戏:你能专心打游戏,有声音就不打。

其实以上三种种情况就类似接下来要说的IO模型:阻塞,非阻塞,异步;还有最后一种情况最后说。

一.阻塞式IO

特点:

1.阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。

2.效率低。进程阻塞,一直等一个任务结束,只能干一件事,效率很低。

3.不浪费CPU。当阻塞的时候,进程就会一直等待,不会浪费CPU资源。

二.非阻塞式IO

当我们设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”

1.当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。

2.应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。

3.这种模式使用中不普遍。

1.使用函数自带参数实现(例:消息队列):

接收端:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
 
struct msgbuf{
	long type;
	int num;
	char ch;
};
 
int main(int argc, const char *argv[])
{
	key_t key;
	key = ftok("2",'a');
	if(key < 0){
		perror("ftok err");
		return -1;
	}
	int msgid = msgget(key,IPC_CREAT | IPC_EXCL | 0777);
	if(msgid < 0){
		if(errno == EEXIST)
			msgid = msgget(key,0777);
		else{
			perror("msgget err");
			return -1;
		}
	}
	printf("msgid:%d\n",msgid);
	struct msgbuf msg2;
//轮询,不是阻塞
	while(1){
//本来msgrcv最后一个参数是0的时候是阻塞的,但最当后一个参数为IPC_NOWAIT就不阻塞,没收到就返回-1
        printf("查看猫眼\n");
		if(msgrcv(msgid,&msg2,sizeof(msg2) - sizeof(long),0,IPC_NOWAIT) >= 0){    //当返回值大于1打印接受值
		    printf("type:%ld num:%d ch:%c 停止打游戏\n",msg2.type,msg2.num,msg2.ch);
		    break;
		}
		else{
            printf("打游戏\n");
			continue;
        }
        sleep(1);        //一秒看一次猫眼
	}
	msgctl(msgid,IPC_RMID,NULL);
	return 0;
}

发送端:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
 
struct msgbuf{
	long type;
	int num;
	char ch;
};
 
int main(int argc, const char *argv[])
{
	key_t key;
	key = ftok("2",'a');
	if(key < 0){
		perror("ftok err");
		return -1;
	}
	int msgid = msgget(key,IPC_CREAT | IPC_EXCL | 0777);
	if(msgid < 0){
		if(errno == EEXIST)
			msgid = msgget(key,0777);
		else{
			perror("msgget err");
			return -1;
		}
	}
	printf("msgid:%d\n",msgid);
	struct msgbuf msg1;
	msg1.type = 1;
	msg1.num = 6;
	msg1.ch = 'c';
	msgsnd(msgid,&msg1,sizeof(msg1) - sizeof(long),0);
	msgctl(msgid,IPC_RMID,NULL);
	return 0;
}

2.通过设置文件描述符的属性设置非阻塞

int fcntl(int fd, int cmd, ... /* arg */ );         //file control

头文件:#include <unistd.h>

               #include <fcntl.h>

功能:设置文件描述符属性

参数:fd:文件描述符

           cmd:设置方式 - 功能选择

                  F_GETFL 获取文件描述符的状态信息 第三个参数化忽略

                  F_SETFL 设置文件描述符的状态信息 通过第三个参数设置

                  O_NONBLOCK 非阻塞

                  O_ASYNC 异步

                  O_SYNC 同步

           arg:设置的值 in

返回值:特殊选择返回特殊值 F_GETFL 返回的状态值(int)

               其他:成功0 失败-1,更新errno

使用:0为例(输入文件标识符)

           0原本:阻塞、读权限、修改或添加非阻塞

int flags=fcntl(0,F_GETFL);           //1.获取文件描述符原有的属性信息

flags = flags | O_NONBLOCK;      //2.修改添加模式为非阻塞

fcntl(0,F_SETFL,flags);                 //3.设置修改后的模式

cmd全部参数:

F_DUPFD复制文件描述符 (指定最小值)
F_DUPFD_CLOEXEC复制文件描述符并原子性设置 FD_CLOEXEC 标志
F_GETFD获取文件描述符标志 (主要是 FD_CLOEXEC)
F_SETFD设置文件描述符标志
F_GETFL获取文件状态标志 (O_RDONLYO_NONBLOCK 等)
F_SETFL设置文件状态标志
F_GETLK测试是否可以获取一个锁 (不实际加锁)
F_SETLK尝试获取或释放锁 (非阻塞)
F_SETLKW尝试获取或释放锁 (阻塞等待)
F_GETOWN获取接收 SIGIO/SIGURG 信号的进程/进程组 ID
F_SETOWN设置接收 SIGIO/SIGURG 信号的进程/进程组 ID

练习:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main(int argc, const char *argv[])
{
	//int flag = fcntl(0,F_SETFL,O_NONBLOCK);        
//我直接给输入文件描述符设置状态非阻塞行不行?不行!!! 
//F_SETFL命令会完全覆盖文件描述符的所有现有标志位,O_NONBLOC只是众多标志位中的一个
	int flag = fcntl(0,F_GETFL);        //先获取全部标志位
	flag |= O_NONBLOCK;                 //再添加标志位(非阻塞)
	fcntl(0,F_SETFL,flag);              //再替换原来标志位
	char buf[32] = "";
	while(1){
		fgets(buf,32,stdin);
		if(!buf[0])
			printf("看猫眼\n打游戏\n");        //猫眼没人打游戏
		else{
			printf("不打游戏\n");            //猫眼有人不打游戏
			break;
		}
		memset(buf,0,sizeof(buf));
		sleep(2);
	}
	return 0;
}

信号驱动IO:

定义:

信号驱动 I/O 是一种异步 I/O 模型,它允许应用程序在文件描述符就绪时(数据可读或可写)通过信号接收通知,而不是主动轮询或阻塞等待。

步骤:

//1. 设置文件描述符和进程号提交给内核驱动

//一旦fd又事件响应,则内核驱动会给进程发送一个SIGIO的信号

fcntl(fd,F_SETOWN,getpid());                //让文件描述符能接收信号

//2. 设置异步通知

int flag;

flag = fcntl(fd, F_GETFL);            //获取原属性

flag |= O_ASYNC;                         //给flag设置异步 O_ASNC 异步

fcnl(fd,F_SETFL,flag);                  //将修改的属性设置进去,此时fd为异步

//3. signal捕捉SIGIO信号

//一旦内核给进程发送SIGI信号,则执行handler

signal(SIGIO,hander);

练习:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <signal.h>
int fd;
void handler(int sig){
	char buf[32] = "";
	read(fd,buf,sizeof(buf));
	printf("%s不打游戏\n",buf);
}

int main(int argc, const char *argv[])
{
	//int flag = fcntl(0,F_SETFL,O_NONBLOCK);
	fd = open("/dev/input/mouse0",O_RDONLY);        //打开鼠标移动文件
	if(fd < 0){
		perror("open err");
		return -1;
	}
	fcntl(fd,F_SETOWN,getpid());	//让信号发送给当前进程,并用fd接收
	//添加异步通知
	int flag = fcntl(fd,F_GETFL);	//获取fd描述符
	flag |= O_ASYNC;				//添加描述符
	fcntl(fd,F_SETFL,flag);			//设置描述符
	signal(SIGIO,handler);            //听门外是否有声音,不影响打游戏

	while(1){
		printf("打游戏\n");
		sleep(2);
	}
	close(fd);
	return 0;
}

当我鼠标移动,就代表门外有声音

三种基础IO模型比较:

阻塞IO(Blocking IO)

非阻塞IO(Non-blocking IO)

信号驱动IO(Signal-driven IO)

同步性

同步

同步

异步

描述

调用IO操作的线程会被阻塞,直到操作完成

调用IO操作时,如果不能立即完成操作,会立即返回,线程可以继续执行其他操作

当IO操作可以进行时,内核会发送信号通知进程

特点

最常见、效率低、不耗费cpu,

轮询、耗费CPU,可以处理多路IO,效率高

异步通知方式,需要底层驱动的支持

适应场景

小规模IO操作,对性能要求不高

高并发网络服务器,减少线程阻塞时间

实时性要求高的应用,避免轮询开销

四.多路复用IO模型(select/poll/epoll)

最后一种情况:

你想要知道是谁回来了,如果是爸妈,停止打游戏,如果是哥哥就出门迎接:让回来的角色来告诉我是谁回来了,但是不影响自己打游戏。

分析:

1.阻塞:一直等待没时间玩。

2.非阻塞(轮询):一直跑来跑去看猫眼,太累了。

3.信号驱动IO:只能识别声音这个信号,不知道谁回来。

4.这个时候就用到多路复用IO:让爸妈或者哥哥回来的时候告诉我谁要回来

介绍:

1.应用程序中同时处理多路输入输出流,若采用阻塞模式,得不到预期的目的;

2.若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;

3.若设置多个进程/线程,分别处理一条数据通路,将新产生进程/线程间的同步与通信问题,使程序变得更加复杂;

4.比较好的方法是使用I/O多路复用技术。其基本思想是:

        先构造一张有关描述符的表(select读表最大1024),然后调用一个函数。

        当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。

        函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。

1.select

特点:

1.一个进程最多只能监听1024个文件描述符

2.select被唤醒之后需要重新轮询,效率相对较低

3.select每次都会清空未发生响应的文件描述符,每次拷贝都需要从用户空间到内核空间,效率低,开销大

编程步骤:

1.先构造一张关于文件描述符的表

2.清空表FD_ZERO

3.将关心的文件描述符添加到表中 FD_SET

4.调用select

5.判断哪一个或者哪些文件描述符发生了事件FD_ISSET

6.做对应的逻辑处理

 

结合代码更好理解。

函数接口:

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

头文件:#include <sys/select.h>
               #include <sys/time.h>
               #include <sys/types.h>
               #include <unistd.h>

功能:实现IO的多路复用

参数:nfds:关注的最大的文件描述符+1

           readfds:关注的读表

           writefds:关注的写表

           exceptfds:关注的异常表

           timeout:超时的设置

                   NULL:一直阻塞,直到有文件描述符就绪或出错

                   时间值为0:仅仅检测文件描述符集的状态,然后立即返回

                   时间值不为0:在指定时间内,如果没有事件发生,则超时返回0,并清空设置的时间值

struct timeval {

        long tv_sec; /* 秒 */

        long tv_usec; /* 微秒 = 10^-6秒 */

};

返回值:成功:时返回准备好的文件描述符的个数

               0:超时检测时间到并且没有文件描述符准备好

               -1 :失败

注:select返回后,关注列表中只存在准备好的文件描述符

操作表:

void FD_CLR(int fd, fd_set *set);         //清除集合中的fd位

void FD_SET(int fd, fd_set *set);         //将fd放入关注列表中

int FD_ISSET(int fd, fd_set *set);         //判断fd是否产生操作 是:1 不是:0

void FD_ZERO(fd_set *set);                //清空关注列表

练习:

基础功能实现

 鼠标文件描述符就相当于爸妈回来的声音,输入文件描述符相当于哥哥回来的声音

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
 
int main(int argc, const char *argv[])
{
	int fd = open("/dev/input/mouse0",O_RDONLY);	//打开鼠标文件
	if(fd < 0){
		perror("open err");
		return -1;
	}
	fd_set readfds;						//创建读表
	FD_ZERO(&readfds);					//清空读表
	FD_SET(fd,&readfds);				//把文件描述符fd放入读表中
	FD_SET(0,&readfds);					//把文件描述符0方式读表中
	if(select(fd+1,&readfds,NULL,NULL,NULL) < 0){	//一直阻塞直到存的文件描述符就绪或者出错
		perror("select err");
		return -1;
	}
	char buf[32] = "";
	if(FD_ISSET(fd,&readfds)){
		ssize_t n = read(fd,buf,sizeof(buf)-1);		//读取鼠标文件内容,并预留一个位置给'\0'
		buf[n] = '\0';								//最后一个位置存'\0'
		printf("mouse:%s\n",buf);
	}
	if(FD_ISSET(0,&readfds)){						//判断终端是否有数据输入
		scanf("%s",buf);
		printf("keybord:%s\n",buf);
	}
	

	return 0;
}

 对鼠标移动和键盘输入进行判断后做出相应反应。

如果想要循环检测,那就得在清空表前加循环,因为select表会交给内核去操作,会改变原表的值,执行完一次就要重新清空添加再监测。

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
 
int main(int argc, const char *argv[])
{
	int fd = open("/dev/input/mouse0",O_RDONLY);	//打开鼠标文件
	if(fd < 0){
		perror("open err");
		return -1;
	}
	fd_set readfds;						//创建读表
	while(1){
		FD_ZERO(&readfds);					//清空读表
		FD_SET(fd,&readfds);				//把文件描述符fd放入读表中
		FD_SET(0,&readfds);					//把文件描述符0方式读表中
		if(select(fd+1,&readfds,NULL,NULL,NULL) < 0){	//一直阻塞直到存的文件描述符就绪或者出错
			perror("select err");
			return -1;
		}
		char buf[32] = "";
		if(FD_ISSET(fd,&readfds)){
			ssize_t n = read(fd,buf,sizeof(buf)-1);		//读取鼠标文件内容,并预留一个位置给'\0'
			buf[n] = '\0';								//最后一个位置存'\0'
			printf("mouse:%s\n",buf);
		}
		if(FD_ISSET(0,&readfds)){						//判断终端是否有数据输入
			scanf("%s",buf);
			printf("keybord:%s\n",buf);
		}	
	}
	close(fd);

	return 0;
}
超时检测: 

概念

什么是网络超时检测呢,比如某些设备的规定,发送请求数据后,如果多长时间后没有收到来自设备的回复,那么需要做出一些特殊的处理

比如: 链接wifi的时候,等了好长时间也没有连接上,此时系统会发送一个消息: 网络连接失败;

必要性:

1.避免进程在没有数据时无限制的阻塞;

2.规定时间未完成语句应有的功能,则会执行相关功能

时间结构:

struct timeval {

        long tv_sec; /* 秒 */

        long tv_usec; /* 微秒 = 10^-6秒 */

};

设置时间,这个结构体是系统自带,直接定义变量使用就可以。

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
 
int main(int argc, const char *argv[])
{
	int fd = open("/dev/input/mouse0",O_RDONLY);	//打开鼠标文件
	if(fd < 0){
		perror("open err");
		return -1;
	}
	fd_set readfds;						//创建读表
	while(1){
		struct timeval tm;					//设置时间
		tm.tv_sec = 3;						//秒
		tm.tv_usec = 0;						//微妙
		FD_ZERO(&readfds);					//清空读表
		FD_SET(fd,&readfds);				//把文件描述符fd放入读表中
		FD_SET(0,&readfds);					//把文件描述符0方式读表中
		if(select(fd+1,&readfds,NULL,NULL,&tm) < 0){	//一直阻塞直到存的文件描述符就绪或者出错
			perror("select err");
			return -1;
		}
		else if(select(fd+1,&readfds,NULL,NULL,&tm) == 0){	//返回值为零,超时
			printf("time over\n");
		}
		char buf[32] = "";
		if(FD_ISSET(fd,&readfds)){
			ssize_t n = read(fd,buf,sizeof(buf)-1);		//读取鼠标文件内容,并预留一个位置给'\0'
			buf[n] = '\0';								//最后一个位置存'\0'
			printf("mouse:%s\n",buf);
		}
		if(FD_ISSET(0,&readfds)){						//判断终端是否有数据输入
			scanf("%s",buf);
			printf("keybord:%s\n",buf);
		}	
	}
	close(fd);

	return 0;
}

2.poll

特点:

1.优化文件描述符的限制,文件描述符的限制取决于系统

2.poll被唤醒之后要重新轮询一遍,效率相对低

3.poll不需要重新构造表,采用结构体数组,每次都需要从用户空间拷贝到内核空间

函数接口:

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

头文件:#include <poll.h>

               #define _GNU_SOURCE 
               #include <signal.h>
               #include <poll.h>

功能:同select相同实现IO的多路复用

参数:fds:指向一个结构体数组的指针,用于指定测试某个给定的文件描述符的条件。

           nfds:指定的第一个参数数组的元素个数。

           timeout:超时设置

                 -1:永远等待

                 0:立即返回

                 >0:等待指定的毫秒数

struct pollfd

{

        int fd; // 文件描述符

        short events; // 等待的事件

        short revents; // 实际发生的事件

};

struct pollfd fds[NUM];

返回值:成功:返回结构体中 revents 域不为 0 的文件描述符个数

              超时:0,超时前没有任何事件发生时,返回 0

              失败:-1,失败并设置 errno

编程步骤:

1.创建一个表, 也就是一个结构体数组 struct pollfd fds[100];

2.将关心的描述符条件到表里并赋予事件

3.循环更新表 while(1) {poll(); }

4.逻辑判断: if(fds[i].revents == POLLIN) {}

练习:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/time.h>
#define NUM 2

int main(int argc, const char *argv[])
{
	int fd = open("/dev/input/mouse0",O_RDONLY);	//打开鼠标文件
	if(fd < 0){
		perror("open err");
		return -1;
	}
	struct pollfd fds[NUM];					//创建表
	fds[0].fd = fd;						//添加文件描述符到结构体内
	fds[0].events = POLLIN;				//设置模式为监听
	fds[1].fd = 0;
	fds[1].events = POLLIN;
	int size = 2;
	while(1){                        //不需要清空状态
		int t = poll(fds,size,2000);    //一直阻塞直到存的文件描述符就绪或者出错,设置时间2000毫秒(两秒)
		if(t < 0){					
			perror("select err");
			return -1;
		}
		else if(t == 0){			//返回值为零,超时
			printf("time over\n");
			continue;
		}
		char buf[32] = "";
		if(fds[0].revents == POLLIN){
			ssize_t n = read(fd,buf,sizeof(buf)-1);		//读取鼠标文件内容,并预留一个位置给'\0'
			buf[n] = '\0';								//最后一个位置存'\0'
			printf("mouse:%s\n",buf);
		}
		if(fds[1].revents == POLLIN){						//判断终端是否有数据输入
			fgets(buf,sizeof(buf),stdin);
			printf("keybord:%s",buf);
		}	
	}
	
	close(fd);
	return 0;
}

超时响应:

鼠标移动:

键盘输入:

注: 因为有revents这个成员,表示实际发生的事件,所以对其他成员没有影响。调用poll后会把实际发生的是按放到revents中,判断它就可以了所以不用清空表

 3.epoll

特点

1. 监听的最大的文件描述符没有个数限制

2. 异步IO,epoll当有事件产生被唤醒之后,文件描述符主动调用callback函数(回调函数)直接拿到唤醒的文件描述符,不需要轮询,效率高

3.epoll不需要重新构造文件描述符表,只需要从用户空间拷贝到内核空间一次。

4.总结

select

poll

epoll

监听个数

一个进程最多监听1024个文件描述符

由程序员自己决定

百万级

方式

每次都会被唤醒,都需要重新轮询

每次都会被唤醒,都需要重新轮询

红黑树内callback自动回调,不需要轮询

效率

文件描述符数目越多,轮询越多,效率越低

文件描述符数目越多,轮询越多,效率越低

不轮询,效率高

原理

每次使用select后,都会清空表

每次调用select,都需要拷贝用户空间的表到内核空间

内核空间负责轮询监视表内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用select,如此循环

不会清空结构体数组

每次调用poll,都需要拷贝用户空间的结构体到内核空间

内核空间负责轮询监视结构体数组内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用poll,如此循环

不会清空表

epoll中每个fd只会从用户空间到内核空间只拷贝一次(上树时)

通过epoll_ctl将文件描述符交给内核监管,一旦fd就绪,内核就会采用callback的回调机制来激活该fd,epoll_wait便可以收到通知(内核空间到用户空间的拷贝

特点

一个进程最多能监听1024个文件描述符

select每次被唤醒,都要重新轮询表,效率低

select每次都清空未发生相应的文件描述符,每次都要拷贝用户空间的表到内核空间

优化文件描述符的个数限制

poll每次被唤醒,都要重新轮询,效率比较低(耗费cpu)

poll不需要构造文件描述符表(也不需要清空表),采用结构体数组,每次也需要从用户空间拷贝到内核空间

监听的文件描述符没有个数限制(取决于自己的系统)

异步IO,epoll当有事件产生被唤醒,文件描述符会主动调用callback函数拿到唤醒的文件描述符,不需要轮询,效率高

epoll不需要构造文件描述符的表,只需要从用户空间拷贝到内核空间一次。

结构

数组

数组

红黑树+就绪链表

开发复杂度

 常见题目

1.标准IO和文件IO的区别是什么?

2.什么是库,静态库和动态库的区别?

3.什么是孤儿进程?什么是僵尸进程?

4.什么是守护进程?

5.进程和线程的区别?

6.线程的同步和互斥,怎么实现?什么是异步?什么是阻塞? 什么是非阻塞?

7.进程间通讯方式有哪些,分别描述一下?效率最高是哪种?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值