非阻塞
- 思考:读普通文件会阻塞吗?
读普通文件时,如果读到了数据就成功返回,如果没有读到数据返回0,总之不会阻塞。
- 思考:写文件时会阻塞吗?
在写某些文件时,当文件不能立即接收写入的数据时,也可能会导致写操作阻塞,一直阻塞到写成功为止。
- 如何实现非阻塞读?
- open()打开文件时指定O_NONBLOCK状态标志;
- 通过fcntl函数指定O_NONBLOCK来实现;
文件锁
- 作用
当多个进程同时读写一个文件时,为了不让进程间相互干扰,我们可以使用进程信号量来互斥实现,除了可以使用进程信号量以外,还可以使用“文件锁”来实现,而且功能更丰富。
- 多进程读写应该满足的关系:
- 写与写互斥。当某个进程正在写文件,而且在数据没有写完时,其它进程不能写,否者会相互打乱对方写的数据。
- 读与写互斥,分两种情况:①某个进程正在写数据时,其它进程不能读数据、②某个进程正在读数据时,其它进程不能写数据。
- 读与读共享。因为读文件是不会修改文件的内容,所以不用担心数据相互干扰的问题。
- 文件锁的读锁与写锁
对文件加锁时可以加两种锁,分别是“读文件锁”和“写文件锁”。读写锁之间的关系也应该满足:
- 读锁和读锁共享:可以重复加读锁,别人加了读锁在没有解锁之前,依然可以加读锁。
- 读锁与写锁互斥:别人加了读锁没有解锁前,加写锁会失败,反过来也是如此。
- 写锁与写锁互斥:别人加了写锁在没有解锁前,不能加写锁,加写锁会失败。
- 文件锁的加锁方式
- 对整个文件内容加锁
- 对文件某部分内容加锁
- 原理
链表上节点代表是一把锁(读锁和写锁),节点存在时表示没有解锁,如果解锁了锁节点就不存在了。锁节点记录了锁的基本信息。加锁时,进程会检查共享的文件锁链表。
与进程信号量比较:
- 进程信号量:进程间共享信号量集合,通过检查集合中信号量的值,从而知道自己能不能操作。
- 文件锁:进程共享文件锁链表,通过检查链表上的锁节点,从而知道自己能不能操作。
- 小tips:
- 在同一进程中,如果多个文件描述符指向同一文件,只要关闭其中任意一个文件描述符,那么该进程加在文件上的文件锁都会被删除,也就是该进程在“文件锁链表”上的“读锁写锁”节点会被删除。(进程终止时会关闭所有打开的fd,所以进程结束时会自动删除所有的文件锁。)
- 父进程所加的文件锁,子进程不会继承 。
- 思考:多线程间能不能使用fcntl实现的文件锁呢?
可以,但是线程不能使用同一个open()返回的fd,线程必须使用自己open()所得到的fd才有效。
- 使用flock()实现文件锁
flock用于多进程时,各进程必须独立open打开文件。需要注意的是对于父子进程,子进程不能使用从父进程继承而来的fd,父子进程flock时必须使用独自open所返回的文件描述符。这一点与fcntl实现的文件锁不一样,父子进程可以使用各自open返回的文件描述符加锁,但是同时子进程也可以使用从父进程继承而来的文件描述符加锁。用于多线程时与用于多进程一样,各线程必须使用各自open所返回的文件描述符才能加锁。
IO多路复用
- 对于多路io来说,只有操作阻塞的fd才有意义。
- 多路IO有什么优势(以同时读鼠标和键盘为例,如果使用):
- 多进程实现。进程切换开销太大,不建议。
- 非阻塞方式。cpu空转,耗费cpu资源,不建议。
- 多线程。常用的一种方法。
- 多路IO。使用多路IO时,多路IO机制由于在监听时如果没有动静的话,监听会休眠,因此开销也很低。相比于多线程,优势在于能够同时管理大量的fd。
select()
- 工作原理:
将用户传入的fd集合拷贝到内核空间,然后查询每个fd对应的设备状态。
- 形参中包含timeout结构体指针,该结构体成员结构为:
struct timeval {
long tv_sec; /* seconds(秒) /
long tv_usec; / microseconds (微秒)*/
};
时间精度为微妙,也就是说可以设置一个精度为微妙级别的超时时间。由于select函数有超时功能,实际上可以使用select模拟出一个微妙级精度的定时器。
- 使用步骤:
- 创建fd_set
- while(1){
用FD_ZERO初始化集合
使用FD_SET往集合中添加fd
ret = select();
if(ret>0){
使用FD_ISSET判断是哪个fd有动静
}
}
- select每次重新监听时需要重新设置“集合”和“超时时间”,因为每次select监听结束时会清空“集合”和“超时时间”。
poll()
由于poll()与select()基本是类似的,所以此处分析下不同点即可:
- 没有最大连接数的限制。
- 每次监听不会清空fd集合,故重新监听时不需要重新配置fd集合。
异步IO
- 原理
进程设置完SIGIO的信号处理函数后就可以去忙其他事,当底层把数据准备好后,内核就会给进程发送SIGIO信号通知进程,该通知会触发信号处理函数去读写数据。
- 使用异步IO时,应用层的设置步骤:
- 调用signal函数对SIGIO信号设置处理函数。
- 使用fcntl函数,将接收SIGIO信号的进程设置为当前进程。如果不设置的,底层驱动并不知道将SIGIO信号发送给哪一个进程。fcntl(mousefd, F_SETOWN, getpid());
- 对文件描述符增设O_ASYNC的状态标志,让fd支持异步IO。两种方式:①open()和②fcntl()
存储映射IO
- 存储映射IO的目的是克服普通读写文件时的缺点,缺点表现在:
使用文件IO的read/write来进行文件的普通读写时,函数经过层层调用后,才能够最终操作到文件,效率低。
- 思考:read/write这种普通读写文件的方式效率不高,那还留着它干嘛?
因为对于数据量较少的情况来说,这种普通读写方式还是非常方便的,而且数据量较少时,效率并不会太低,只有当数据量非常大时,效率的影响才会非常的明显。
- 如何克服read/write的缺点?
使用mmap()实现存储映射IO。map除了“地图”还有“映射”的意思。因为“地图”本身就真实地理环境的映射,所以地图与映射本质是一回事。
- 原理:
将一个磁盘文件映射到进程空间中的一片内存中,于是对文件的读写操作就转变为了对缓冲区的读写,这样,就可以在不使用read/write的情况下进行IO,省去了繁杂的中间调用过程,便于对大量数据的高效IO。
- 思考:使用mmap()时,read/write被省掉了,open是不是也被省掉了?
open不能省,必须要将文件open后,才能使用mmap进行映射。
- 与共享内存的区别:
共享内存是内存与内存间的映射。mmap()是硬盘与内存间的映射。
- tip:mmap()映射大小为0的文件时会失败,映射失败时内核会向进程发送一个SIGBUS信号。