非阻塞

  • 思考:读普通文件会阻塞吗?

读普通文件时,如果读到了数据就成功返回,如果没有读到数据返回0,总之不会阻塞。

  • 思考:写文件时会阻塞吗?

在写某些文件时,当文件不能立即接收写入的数据时,也可能会导致写操作阻塞,一直阻塞到写成功为止。

  • 如何实现非阻塞读?
  1. open()打开文件时指定O_NONBLOCK状态标志;
  2. 通过fcntl函数指定O_NONBLOCK来实现;

文件锁

  • 作用

当多个进程同时读写一个文件时,为了不让进程间相互干扰,我们可以使用进程信号量来互斥实现,除了可以使用进程信号量以外,还可以使用“文件锁”来实现,而且功能更丰富。

  • 多进程读写应该满足的关系:
  1. 写与写互斥。当某个进程正在写文件,而且在数据没有写完时,其它进程不能写,否者会相互打乱对方写的数据。
  2. 读与写互斥,分两种情况:①某个进程正在写数据时,其它进程不能读数据、②某个进程正在读数据时,其它进程不能写数据。
  3. 读与读共享。因为读文件是不会修改文件的内容,所以不用担心数据相互干扰的问题。
  • 文件锁的读锁与写锁

对文件加锁时可以加两种锁,分别是“读文件锁”和“写文件锁”。读写锁之间的关系也应该满足:

  1. 读锁和读锁共享:可以重复加读锁,别人加了读锁在没有解锁之前,依然可以加读锁。
  2. 读锁与写锁互斥:别人加了读锁没有解锁前,加写锁会失败,反过来也是如此。
  3. 写锁与写锁互斥:别人加了写锁在没有解锁前,不能加写锁,加写锁会失败。
  • 文件锁的加锁方式
  1. 对整个文件内容加锁
  2. 对文件某部分内容加锁
  • 原理

链表上节点代表是一把锁(读锁和写锁),节点存在时表示没有解锁,如果解锁了锁节点就不存在了。锁节点记录了锁的基本信息。加锁时,进程会检查共享的文件锁链表。

与进程信号量比较:

  1. 进程信号量:进程间共享信号量集合,通过检查集合中信号量的值,从而知道自己能不能操作。
  2. 文件锁:进程共享文件锁链表,通过检查链表上的锁节点,从而知道自己能不能操作。
  • 小tips:
  1. 在同一进程中,如果多个文件描述符指向同一文件,只要关闭其中任意一个文件描述符,那么该进程加在文件上的文件锁都会被删除,也就是该进程在“文件锁链表”上的“读锁写锁”节点会被删除。(进程终止时会关闭所有打开的fd,所以进程结束时会自动删除所有的文件锁。)
  2. 父进程所加的文件锁,子进程不会继承 。
  • 思考:多线程间能不能使用fcntl实现的文件锁呢?

可以,但是线程不能使用同一个open()返回的fd,线程必须使用自己open()所得到的fd才有效。

  • 使用flock()实现文件锁

flock用于多进程时,各进程必须独立open打开文件。需要注意的是对于父子进程,子进程不能使用从父进程继承而来的fd,父子进程flock时必须使用独自open所返回的文件描述符。这一点与fcntl实现的文件锁不一样,父子进程可以使用各自open返回的文件描述符加锁,但是同时子进程也可以使用从父进程继承而来的文件描述符加锁。用于多线程时与用于多进程一样,各线程必须使用各自open所返回的文件描述符才能加锁。

IO多路复用

  • 对于多路io来说,只有操作阻塞的fd才有意义。
  • 多路IO有什么优势(以同时读鼠标和键盘为例,如果使用):
  1. 多进程实现。进程切换开销太大,不建议。
  2. 非阻塞方式。cpu空转,耗费cpu资源,不建议。
  3. 多线程。常用的一种方法。
  4. 多路IO。使用多路IO时,多路IO机制由于在监听时如果没有动静的话,监听会休眠,因此开销也很低。相比于多线程,优势在于能够同时管理大量的fd。

select()

  • 工作原理:

将用户传入的fd集合拷贝到内核空间,然后查询每个fd对应的设备状态。

  • 形参中包含timeout结构体指针,该结构体成员结构为:

struct timeval {
 long tv_sec; /* seconds(秒) /
 long tv_usec; /
microseconds (微秒)*/
};
时间精度为微妙,也就是说可以设置一个精度为微妙级别的超时时间。由于select函数有超时功能,实际上可以使用select模拟出一个微妙级精度的定时器。

  • 使用步骤:
  1. 创建fd_set
  2. while(1){
     用FD_ZERO初始化集合
     使用FD_SET往集合中添加fd
     ret = select();
     if(ret>0){
      使用FD_ISSET判断是哪个fd有动静
      }
    }
  • select每次重新监听时需要重新设置“集合”和“超时时间”,因为每次select监听结束时会清空“集合”和“超时时间”。

poll()

由于poll()与select()基本是类似的,所以此处分析下不同点即可:

  1. 没有最大连接数的限制。
  2. 每次监听不会清空fd集合,故重新监听时不需要重新配置fd集合。

异步IO

  • 原理

进程设置完SIGIO的信号处理函数后就可以去忙其他事,当底层把数据准备好后,内核就会给进程发送SIGIO信号通知进程,该通知会触发信号处理函数去读写数据。

  • 使用异步IO时,应用层的设置步骤:
  1. 调用signal函数对SIGIO信号设置处理函数。
  2. 使用fcntl函数,将接收SIGIO信号的进程设置为当前进程。如果不设置的,底层驱动并不知道将SIGIO信号发送给哪一个进程。fcntl(mousefd, F_SETOWN, getpid());
  3. 对文件描述符增设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信号。