概述
文章目录
- 前言
- 1.非阻塞IO
- 2.阻塞式IO的困境
- 3.并发式IO的解决方案
- 4.IO多路复用原理
- 5.异步IO
- 6.存储映射IO
前言
本文主要讲了非阻塞的三种方法:
1.并发式IO
2.多路复用IO
3.异步IO
根据朱有鹏相关课程整理
下面是应用编程整理的文章
linux应用编程基础:
1.文件IO
2.文件属性
3.获取系统信息
4.linux进程
5.linux中的信号
6.高级IO
7.linux线程
8.网络基础
1.非阻塞IO
1.1、阻塞与非阻塞
1.2、为什么有阻塞式
(1)常见的阻塞:wait、pause、sleep等函数;read或write某些文件时
(2)阻塞式的好处
1.3、非阻塞
(1)为什么要实现非阻塞,在多路IO时需要非阻塞式。
(2)如何实现非阻塞IO访问:O_NONBLOCK(open函数标志)和fcntl函数
2.阻塞式IO的困境
2.1、程序中读取键盘,键盘是标准输入设备,文件描述符为0
2.2、程序中读取鼠标,鼠标不是标准输入设备,鼠标这个设备在/dev/input/mouse0中,有三个鼠标设备,我们cat mouse0,被阻塞住,然后晃动鼠标,发现控制台有输出,则我们当前使用的就是这个鼠标设备,鼠标输入的内容是二进制格式,所以是乱码。
2.3、程序中同时读取键盘和鼠标
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
// 读取鼠标
int fd = -1;
char buf[200];
fd = open("/dev/input/mouse0", O_RDONLY); //每台设备的鼠标名称不一样
if (fd < 0){
perror("open:");
return -1;
}
memset(buf, 0, sizeof(buf));
printf("before 鼠标 read.n");
read(fd, buf, 50);
printf("鼠标读出的内容是:[%s].n", buf);
// 读键盘
memset(buf, 0, sizeof(buf));
printf("before 键盘 read.n");
read(0, buf, 5);
//键盘是标准输入,fd=0
printf("键盘读出的内容是:[%s].n", buf);
return 0;
}
在阻塞的情况下只能鼠标读好了之后再去读键盘,这显然和 “我们要同时收到两个input设备的输入” 的诉求不符。
3.并发式IO的解决方案
3.1、非阻塞式IO,打开鼠标设备文件时添加O_NONBLOCK标志即可,但是键盘本身就是打开的,所以我们用fcntl函数来改变他的阻塞属性:
flag = fcntl(0, F_GETFL); // 先获取原来的flag
flag |= O_NONBLOCK; // 添加非阻塞属性
fcntl(0, F_SETFL, flag); // 更新flag
一个while(1)循环搞定一切,加上判断语句是否有输入,即可实现并发IO,缺点是用死循环实现,性能不高。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
// 读取鼠标
int fd = -1;
int flag = -1;
char buf[200];
int ret = -1;
fd = open("/dev/input/mouse0", O_RDONLY | O_NONBLOCK);
if (fd < 0)
{
perror("open:");
return -1;
}
// 把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(buf, 0, sizeof(buf));
//printf("before 键盘 read.n");
ret = read(0, buf, 5);
if (ret > 0)
printf("键盘读出的内容是:[%s].n", buf);
}
return 0;
}
4.IO多路复用原理
4.1、何为IO多路复用
(1)IO multiplexing
(2)用在什么地方?多路非阻塞式IO。
(3)select和poll
(4)外部阻塞式,内部非阻塞式自动轮询多路阻塞式IO。解释:比如鼠标输入事件和键盘输入事件就是阻塞式IO,多路复用就有点类似与操作系统任务调度,将两个阻塞式的IO封装起来,在内部自动轮询,达到内部非阻塞式的效果,但是在外部看来是阻塞式的,这就叫外部阻塞式,内部非阻塞式自动轮询多路阻塞式IO。
4.2、select函数介绍
/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
struct timeval {
long
tv_sec;
/* seconds */
long
tv_usec;
/* microseconds */
};
struct timespec {
long
tv_sec;
/* seconds */
long
tv_nsec;
/* nanoseconds */
};
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);
void FD_ZERO(fd_set *set);
4.3、poll函数介绍
poll的机制与select类似,与select在本质上没有多大差别;同样是轮询多个描述符,再根据描述符的状态进行处理;但是poll没有描述符数量的限制,这个与机器的上限有关;缺点与select类似即需要将大量的描述符从用户态复制至内核态;而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增加。
返回值: 成功时,poll() 返回结构体中 revents 域不为 0 的文件描述符个数;如果在超时前没有任何事件发生,poll()返回 0; 失败时,poll() 返回 -1,并设置 errno 为下列值之一:
EBADF:一个或多个结构体中指定的文件描述符无效。
EFAULT:fds 指针指向的地址超出进程的地址空间。
EINTR:请求的事件之前产生一个信号,调用可以重新发起。
EINVAL:nfds参数超出 PLIMIT_NOFILE 值。
ENOMEM:可用内存不足,无法完成请求。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int
fd;
/* file descriptor */
//表示我们要监测什么文件
short events;
/* requested events */
//表示我们要监测文件的什么事件
short revents;
/* returned events */
//是内核设计好的,到时候和events比较来判断此IO有没有发生
};
事件 events
POLLIN 有数据可读
POLLRDNORM 有普通数据可读
POLLRDBAND 有优先数据可读
POLLPRI有紧急数据可读
POLLOUT 数据可写
POLLWRNORM 普通数据可写
POLLWRBAND 优先数据可写
POLLMSGSIGPOLL 消息可用
返回事件 revent
除了 事件外;还有
POLLERR 指定描述符发生错误
POLLHUP 指定文件描述符挂起事件
POLLNVAL 指定描述符非法
4.4、用select函数实现同时读取键盘鼠标
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
int main(void)
{
int fd = -1, ret = -1;
char buf[200];
//fd_set结构体在man中没有给出,是文件描述符的集合
fd_set myset;
struct timeval tm;
fd = open("/dev/input/mouse1", O_RDONLY);
if (fd < 0)
{
perror("open:");
return -1;
}
// 当前有2个fd,一个是fd一个是0
// 处理myset
//FD_ZERO:在向结构体fd_set添加之前,先清理一下,就像用变量之前先memset一下一样
//FD_SET:将文件描述符添加到结构体fd_set中
FD_ZERO(&myset);
FD_SET(fd, &myset);
FD_SET(0, &myset);
tm.tv_sec = 10;
tm.tv_usec = 0;
ret = select(fd+1, &myset, NULL, NULL, &tm);
//fd从0开始,所以要+1,让select函数能遍历到所有文件描述符
if (ret < 0)
{
perror("select: ");
return -1;
}
else if (ret == 0)
printf("超时了n");
else
{
// 等到了一路IO,然后去监测到底是哪个IO到了,处理之
if (FD_ISSET(0, &myset))
{
// 这里处理键盘
memset(buf, 0, sizeof(buf));
read(0, buf, 5);
printf("键盘读出的内容是:[%s].n", buf);
}
if (FD_ISSET(fd, &myset))
{
// 这里处理鼠标
memset(buf, 0, sizeof(buf));
read(fd, buf, 50);
printf("鼠标读出的内容是:[%s].n", buf);
}
}
return 0;
}
4.5、用poll函数实现同时读取键盘鼠标
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>
int main(void)
{
// 读取鼠标
int fd = -1, ret = -1;
char buf[200];
//结构体数组,我们有两个文件描述符,对应两个结构体
struct pollfd myfds[2] = {0};
fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open:");
return -1;
}
// 初始化我们的pollfd
myfds[0].fd = 0;
// 键盘
myfds[0].events = POLLIN; // 等待读操作
myfds[1].fd = fd;
// 鼠标
myfds[1].events = POLLIN; // 等待读操作
ret = poll(myfds, fd+1, 10000);
if (ret < 0)
{
perror("poll: ");
return -1;
}
else if (ret == 0)
printf("超时了n");
else
{
// 等到了一路IO,然后去监测到底是哪个IO到了,处理之
if (myfds[0].events == myfds[0].revents)
{
// 这里处理键盘
memset(buf, 0, sizeof(buf));
read(0, buf, 5);
printf("键盘读出的内容是:[%s].n", buf);
}
if (myfds[1].events == myfds[1].revents)
{
// 这里处理鼠标
memset(buf, 0, sizeof(buf));
read(fd, buf, 50);
printf("鼠标读出的内容是:[%s].n", buf);
}
}
return 0;
}
5.异步IO
5.1、何为异步IO
(1)几乎可以认为:异步IO就是操作系统用软件实现的一套中断响应系统。
(2)异步IO的工作方法是:我们当前进程注册一个异步IO事件(使用signal注册一个信号SIGIO的处理函数),然后当前进程可以正常处理自己的事情,当异步事件发生后当前进程会收到一个SIGIO信号从而执行绑定的处理函数去处理这个异步事件。
6.2、涉及的函数:
(1)fcntl(F_GETFL、F_SETFL、O_ASYNC、F_SETOWN)
(2)signal或者sigaction(SIGIO)
#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)
return;
read(mousefd, buf, 50);
printf("鼠标读出的内容是:[%s].n", buf);
}
int main(void)
{
// 读取鼠标
char buf[200];
int flag = -1;
mousefd = open("/dev/input/mouse1", O_RDONLY);
if (mousefd < 0)
{
perror("open:");
return -1;
}
// 把鼠标的文件描述符设置为可以接受异步IO
flag = fcntl(mousefd, F_GETFL);
flag |= O_ASYNC;
fcntl(mousefd, F_SETFL, flag);
// 把异步IO事件的接收进程设置为当前进程
fcntl(mousefd, F_SETOWN, getpid());
// 注册当前进程的SIGIO信号捕获函数
signal(SIGIO, func);
// 读键盘
while (1)
{
memset(buf, 0, sizeof(buf));
//printf("before 键盘 read.n");
read(0, buf, 5);
printf("键盘读出的内容是:[%s].n", buf);
}
return 0;
}
fcntl函数详解
头文件:
#include <unistd.h>、#include <fcntl.h>
函数原型:
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
函数功能:
1.复制一个现有的描述符(cmd=F_DUPFD).
2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
3.获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
4.获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
6.存储映射IO
6.1、mmap函数
函数原型 :
void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);
参数:
addr:建立映射区的首地址,有Linux内核指定。使用时传NULL即可
length:与创建映射取的大小
prot: 映射区权限PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE、PROT_EXEC 映射区域可被执行、PROT_NONE 映射区域不能存取
flags:标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
MAP_SHARED对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。
MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)。
fd: 用来建立映射区的文件描述符
1、普通映射:要映射到内存中的文件描述符
2、匿名内存映射:fd设为-1,即flags中设置了MAP_ANONYMOUS
//有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,然后对该文件进行映射,可以同样达到匿名内存映射的效果。
offset: 映射文件的偏移(4k的整数倍,分页大小的整数倍)
8、错误代码errno :
EBADF 参数fd无效
EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE以及该文件要能写入。
EINVAL 参数start、length 或offset有一个不合法。
EAGAIN 文件被锁住,或是有太多内存被锁住。
ENOMEM 内存不足。
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
MAP_FIXED :使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
MAP_SHARED :对映射区域的写入数据会复制回文件内, 而且允许其他映射该文件的进程共享。
MAP_PRIVATE :建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
MAP_DENYWRITE :这个标志被忽略。
MAP_EXECUTABLE :同上
MAP_NORESERVE :不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
MAP_LOCKED :锁定映射区的页面,从而防止页面被交换出内存。
MAP_GROWSDOWN :用于堆栈,告诉内核VM系统,映射区可以向下扩展。
MAP_ANONYMOUS :匿名映射,映射区不与任何文件关联。
MAP_ANON :MAP_ANONYMOUS的别称,不再被使用。
MAP_FILE :兼容标志,被忽略。
MAP_32BIT :将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
MAP_POPULATE :为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
MAP_NONBLOCK :仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
返回说明
成功执行时,mmap()返回被映射区的指针,munmap()返回0。失败时,mmap()返回MAP_FAILED[其值为(void *)-1],munmap返回-1。errno被设为以下的某个值
EACCES:访问出错
EAGAIN:文件已被锁定,或者太多的内存已被锁定
EBADF:fd不是有效的文件描述词
EINVAL:一个或者多个参数无效
ENFILE:已达到系统对打开文件的限制
ENODEV:指定文件所在的文件系统不支持内存映射
ENOMEM:内存不足,或者进程已超出最大内存映射数量
EPERM:权能不足,操作不允许
ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
SIGSEGV:试着向只读区写入
SIGBUS:试着访问不属于进程的内存区
munmap函数
当不需要这个映射内存时通过munmap函数来释放
函数原型:
int munmap(void *addr, size_t length);
参数:
addr :mmap的返回值
length:释放区域的长度
例子:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>
typedef struct _Student{
int sid;
char sname[20];
}Student;
int main(int argc,char *argv[])
{
//open file
int fd = open(argv[1],O_RDWR);
//mmap
int length = sizeof(Student);
Student *stu = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(stu == MAP_FAILED){
perror("mmap err");
return -1;
}
//read data
while(1){
printf("sid=%d,sname=%sn",stu->sid,stu->sname);
sleep(1);
}
//close and munmap
munmap(stu,length);
close(fd);
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>
typedef struct
_Student{
int sid;
char sname[20];
}Student;
int main(int argc,char *argv[])
{
if(argc != 2){
printf("./a.out filenamen");
return -1;
}
// 1. open file
int fd = open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666);
int length = sizeof(Student);
ftruncate(fd,length);
//设置文件大小为结构体大小
// 2. mmap
Student * stu = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(stu == MAP_FAILED){
perror("mmap err");
return -1;
}
int num = 1;
// 3. 修改内存数据
while(1){
stu->sid = num;
sprintf(stu->sname,"xiaoming-%03d",num++);
sleep(1);//相当于每隔1s修改一次映射区的内容
}
// 4. 释放映射区和关闭文件描述符
munmap(stu,length);
close(fd);
return 0;
}
mmap的一些问题:
1.如果对mem越界操作会怎样? 多出的数据会丢失,尽量避免这样的操作2.如果文件描述符向关闭,对mmap映射有没有影响 没有影响
3.open的时候,可以新创建一个文件来创建映射区吗? 不可以,使用的文件大小不能为0
4.如果文件偏移随便填一个数会怎么样? 运行时会报错
可以直接用内存操作函数对文件进行读写,比如strcpy,memset,memcpy
匿名映射
每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlink,close掉,比较麻烦。Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件,即可创建映射取。将flags设为-1即可。
6.2、LCD显示和IPC之共享内存
6.3、存储映射IO的特点
(1)共享而不是复制,减少内存操作
(2)处理大文件时效率高,小文件不划算
详细参考文章:
https://blog.csdn.net/qq_42024067/article/details/104498825
https://www.jianshu.com/p/5a663300589c
最后
以上就是高贵月饼为你收集整理的linux高级IO,阻塞与非阻塞前言1.非阻塞IO2.阻塞式IO的困境3.并发式IO的解决方案4.IO多路复用原理5.异步IO6.存储映射IO的全部内容,希望文章能够帮你解决linux高级IO,阻塞与非阻塞前言1.非阻塞IO2.阻塞式IO的困境3.并发式IO的解决方案4.IO多路复用原理5.异步IO6.存储映射IO所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复