目录
1.非阻塞IO
1.1.阻塞读文件
读某些文件时,如果文件没有数据的话,往往会导致读操作会阻塞(休眠)。
(1)读鼠标、键盘等字符设备文件
(2)读管道文件(有名无名)
(1)疑问:读普通文件会阻塞吗?读普通文件时,如果读到了数据就成功返回,如果没有读到数据返回0,总之不会阻塞。
(2)疑问:写文件时会阻塞吗?
在写某些文件时,当文件不能立即接收写入的数据时,也可能会导致写操作阻塞,一直阻塞到写成功为止。
1.2.如何实现非阻塞读
(1)打开文件时指定O_NONBLOCK状态标志
fd = open("/dev/input/mouse0", O_RDONLY|O_NONBLOCK);
(2)通过fcntl函数指定O_NONBLOCK来实现
什么情况下会使用fcntl来实现
1)情况1:当文件已经被open打开了,但是open是并没有指定你要的文件状态标志
2)情况2:没办法在open指定,你手里只有一个文件描述符fd,此时就使用fcntl来重设或者补设
当然我们使用fcntl不仅仅只能重设或者补设O_NONBLOCK,也可以重设或者补设O_TRUNC/O_APPEND等任何你需要的“文件状态”标志。
例子:将0设置为O_NONBLOCK
//· 重设 fcntl(0, F_SETFL, O_RDONLY|O_NONBLOCK); //· 补设 flag = fcntl(0, F_GETFL); //获取原有文件状态标志 flag = flag | O_NONBLOCK; //通过|操作,在已有的标志上增设O_NONBLOCK fcntl(0, F_SETFL, flag); //将修改后的“文件状态标志”设置回去
1.3.实现同时“读鼠标”和“读键盘”
(1)fork子进程,然后父子进程两线任务
(2)创建次线程,主线程和次线程两线任务
(3)将鼠标和键盘设置为“非阻塞”,while轮询的读。
2.文件锁
2.1.文件锁的作用
顾名思义,就是用来保护文件数据的。
当多个进程共享读写同一个文件时,为了不让进程们各自读写数据时相互干扰,我们可以使用进程信号量来互斥实现,除了可以使用进程信号量以外,还可以使用“文件锁”来实现,而且功能更丰富,使用起来相对还更容易些。
2.2.多进程读写文件
多进程共享读写同一个文件时,如果数据很重要的话,为了防止数据相互修改,应该满足如下读写条件:
(1)写与写应该互斥
(2)读与写也应该是互斥的
1)某个进程正在写数据,而且在数据没有写完时,其它进程不能读数据
2)某个进程正在读数据,在数据没有读完之前,其它进程不能写数据
(3)读与读共享
2.3.文件锁
2.3.1.文件锁的读锁与写锁
读锁、写锁之间关系
(1)读锁和读锁共享:可以重复加读锁,别人加了读锁在没有解锁之前,我依然可以加读锁,这就是共享。
(2)读锁与写锁互斥:别人加了读锁没有解锁前,加写锁会失败,反过来也是如此。
加锁失败后两种处理方式
- 阻塞,直到别人解锁然后加锁成功为止
- 出错返回,不阻塞(3)写锁与写锁互斥:别人加了写锁在没有解锁前,不能加写锁,加写锁会失败。
2.3.2.使用文件锁对文件进行保护
读文件时加读锁,写文件时就加写锁,然后就可以很容易的实现符合如下要求的资源保护。
1)写与写之间互斥
2)读与写之间互斥
3)读与读之间共享
2.3.3.文件锁的加锁方式
(1)对整个文件内容加锁
(2)对文件某部分内容加锁
2.4.文件锁的实现
实现文件锁时,我们还是需要使用fcntl函数。
2.4.1.fcntl的函数实现
#include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd, .../*struct flock *flockptr */ );
(1)功能
- fcntl函数有多种功能,这里主要介绍实现文件锁的功能,
- 当cmd被设置的是与文件锁相关的宏时,fcntl就是用于实现文件锁。
(2)返回值:成功返回0,失败则返回-1,并且errno被设置。
(3)参数
struct flock结构体
struct flock { short l_type; // Type of lock: F_RDLCK,F_WRLCK, F_UNLCK short l_whence; //How to interpret l_start:SEEK_SET, SEEK_CUR, SEEK_END off_t l_start; // Starting offset for lock off_t l_len; //Number of bytes to lock pid_t l_pid; //PID of process blocking our lock(F_GETLK only) }
成员说明:
2.4.2.文件锁的原理
链表上节点代表是一把锁(读锁和写锁),节点存在时表示没有解锁,如果解锁了锁节点就不存在了
锁节点记录了锁的基本信息。
· 锁类型
· 加锁的起始位置(l_whence、l_start)
· 加锁的长度(l_len)
· 当前正在加着锁的那个进程的PID
加锁时,进程会检查共享的文件锁链表。
(1)进程想加读锁
1)如果链表上只有读锁节点
表明目前其它进程对该文件只加了读锁,由于读锁共享,所以不管链表上有几个读锁节点,当前进程都能成功加读锁。
2)如果链表上有一个写锁节点
表明目前有进程对文件加了写锁,锁节点还存在,表示人家目前还没有解锁,读锁和写锁是互斥的,所以当前不能加读锁,别人解锁后才能加读锁,加锁后链表上就插入一个读锁节点。
(2)你想加写锁
1)如果链表上有读锁节点,别人还没有解锁,读锁与写锁互斥,不能加写锁。
2)如果链表上有写锁节点,别人还没有解锁,写锁与写锁互斥,多以当前进程不能加写锁
(3)对比进程信号量
1)进程信号量:进程间共享信号量集合,通过检查集合中信号量的值,从而知道自己能不能操作
2)文件锁: 进程共享文件锁链表,通过检查链表上的锁节点,从而知道自己能不能操作
2.4.3.文件锁其它值得注意的地方
(a)在同一进程中,如果多个文件描述符指向同一文件,只要关闭其中任何一个文件描述符
那么该进程加在文件上的所有文件锁将会被删除,也就是该进程在“文件锁链表”上的“读锁写锁”节点会被删除。、
进程终止时会关闭所有打开的文件描述符,所以进程结束时会自动删除所有加的文件锁。
(b)父进程所加的文件锁,子进程不会继承
多线程间能不能使用fcntl实现的文件锁呢?
可以,但是线程不能使用同一个open返回的文件描述符,线程必须使用自己open所得到的文件描述符才有效。
2.4.4.使用flock函数来实现文件锁
flock与fcntl所实现的文件锁一样,既能够用在多进程上,也能用在多线程上,而且使用起来比fcntl的实现
#include<sys/file.h> int flock(int fd, int operation);
1)用于多进程
flock用于多进程时,各进程必须独立open打开文件
需要注意的是亲缘进程(父子进程),子进程不能使用从父进程继承而来的文件描述符,
父子进程flock时必须使用独自open所返回的文件描述符。
这一点与fcntl实现的文件锁不一样,父子进程可以使用各自open返回的文件描述符加锁,、
但是同时子 进程也可以使用从父进程继承而来的文件描述符加锁。
2)用于多线程
用于多线程时与用于多进程一样,各线程必须使用各自open所返回的文件描述符才能加锁。
3.io多路复用
3.1.有关多路IO
(1)多路IO的工作原理
我们以阻塞读为例
如果是阻塞写的话,需要将文件描述符加入写集合,不过我们说过对于99%的情况来说,写操作不会阻塞
所以一般情况下对于写来说,使用多路Io没有意义。注意:对于多路io来说,只有操作阻塞的fd才有意义,如果文件描述符不是阻塞的,使用多路IO没有意义。
(2)多路IO有什么优势
比如以同时读写鼠标、读键盘为例,如果使用,
(3)select和poll
多路IO有两种实现方式,分别是poll和select,其中select会比poll更常用些。
3.2.多路io之select机制
#include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
1)功能
2)参数
struct timeval { long tv_sec; /* seconds(秒) */ long tv_usec; /* microseconds (微秒)*/ }; //· tv_sec:设置秒 //· tv_usec:设置微妙 //时间精度为微妙,也就是说可以设置一个精度为微妙级别的超时时间。
3)返回值
select每次重新监听时需要重新设置“集合”和“超时时间”,因为每次select监听结束时会清空“集合”和“超时时间”。
3.3.多路io 之 poll机制
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:
参数:
poll监听时如果没有动静就阻塞,有动静就不再阻塞,返回有动静的fd的数量。
如何知道是那些fd有动静?
如果文件描述符“发生的事件”==“实际事件”,就说明希望的事件来了,就是对fd进行相应的“读或写”操作
返回值:
4.异步io
4.1.回顾同时读键盘、鼠标的方法
- 1)多进程
- 2)多线程
- 3)将“读鼠标”和“读键盘”设置为非阻塞
- 4)多路IO(select、poll机制)
- 5)异步IO
4.2.异步IO的原理
前面四种方式都是主动的去读,对于read函数来说它并不知道是不是一定有数据,如果有数据就读到数据,没有数据要么阻塞直到读到数据为止,要么就不阻塞。
异步IO的原理就是,底层把数据准备好后,内核就会给进程发送一个“异步通知的信号”通知进程,表示数据准备好了,然后调用信号处理函数去读数据,在没有准备好时,进程忙自己的事情。
4.3.使用异步IO方式读鼠标和键盘
进程正常阻塞读键盘,然后将读鼠标设置为异步IO方式。
进程正常阻塞读键盘时,如果鼠标没有数据的话,进程不关心读鼠标的事情,如果鼠标数据来了,底层鼠标驱动就会向进程发送一个SIGIO信号,然后调用注册的SIGIO信号捕获函数读鼠标数据。
当然也可以反过来,进程正常阻塞读鼠标,然后将读键盘设置为异步IO方式。
不过使用异步IO有两个前提
(1)底层驱动必须要有相应的发送SIGIO信号的代码,只有这样当底层数据准备好后,底层才会发送SIGIO信号给进程。
(2)应用层必须进行相应的异步IO的设置,否者无法使用异步IO
应用层进行异步IO设置时,使用的也是fcntl函数。
4.4.使用异步IO时,应用层的设置步骤
5.存储映射
5.1.普通读写文件方式的缺点
5.1.1.普通读写文件的特点
使用文件IO的read/write来进行文件的普通读写时,函数经过层层的调用后,才能够最终操作到文件,
从应用缓存到文件,效率很低
(1)cpu执行一堆的函数,很耗费cpu资源,而且浪费时间
(2)中间一堆的缓存都是函数从内存开辟的,浪费内存资源,而且数据在各缓存间倒腾时也很耗费时间
5.1.2.普通读写方式的缺点:面对大量数据时显得很吃力
5.2.存储映射所用的mmap函数
5.2.1.mmap(memory map)的原理
mmap的原理就是,既然直接read、write很费劲,那我干脆抛弃read、write的操作,mmap采用直接映射的方式实现
mmap映射时,比如映射普通文件,其实就会将普通文件的硬盘空间的物理地址映射到进程空间的虚拟地址。
通常情况下,进程空间的虚拟地址只映射自己底层物理空间的物理地址,但是使用mmap时,他会将文件的硬盘空间的地址也映射到虚拟地址空间
这么一来应用程序就可以直接通过映射的虚拟地址操作文件,根本就不需要read、write函数了,使用地址操作时省去了繁杂的中间调用过程,可以快速对文件进行大量数据的输入输出。
疑问:使用存储映射时,read、write被省掉了,open是不是也被省掉了?
答:open不能省,必须要将文件open后,才能使用mmap进行映射。
5.2.2.映射时,具体映射到了进程空间的什么位置呢?
映射到了“进程应用空间”堆和栈中间那片虚拟地址的位置。
(1)进程内核空间:用于映射“OS”所在的物理内存空间
(2)进程应用空间:用于映射“应用程序”所在的物理内存空间
5.2.3.对比IPC之共享内存
1)存储映射,其实也可以用来实现进程间通信
2)虽然存储映射和共享内存原理相似,但是各自用途不同
5.2.4.mmap函数
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
5.2.5.munmap
#include <sys/mman.h> int munmap(void *addr, size_t length);