一、非阻塞IO
系统调用被分为低速系统调用和其他系统调用。低速系统调用的意思是有可能会永远阻塞进程。有以下几种低速系统调用:
- 如果某些文件类型的数据不存在,读操作可能会永远阻塞
2.如果数据不能被某些文件类型立即接受,写操作也可能会被永远阻塞
3.打开某种类型的文件必须等到某条件达成
4.对已经加上强制性记录锁的文件进行读写
5.某些ioctl操作
6.某些进程间通信函数
而非阻塞IO操作则可以让我们安全的使用read、write等函数,使其永远不会阻塞。这些操作如果不能立即完成,则会立刻报错返回。对于一个给定的文件描述符,有两种办法对其进行非阻塞操作:
1.如果调用open获得描述符,则可指定O_NONBLOCK标志
2.对于一个已经打开的文件描述符,则可以调用fcntl,由该函数打开O_NONBLOCK标志文件。
二、记录锁
记录锁保证某文件某一时刻某一块区域只有一个进程在修改,更合适的名称应该是字节范围i锁。
记录锁使用fcntl:
cpp
#include <fcntl.h>
fcntl(int filedes,int cmd , ......);
对于记录锁,cmd应传入的参数是F_GETLK、F_SETLK和F_SETLKW,第三个参数是一个指向flock结构的指针:
cpp
struct flock{
short l_type;//F_RDLCK,F_WRLCK,F_UNLCK
off_t l_start;
short l_whence;
off_t l_len;
pid_t l_pid;
}
-
l_type:希望的锁的类型F_RDLCK(共享性读锁),F_WRLCK(独占性写锁),F_UNLCK(解锁一个区域)
-
l_start:要加锁或解锁的区域的字节偏移量,与l_whence共同决定
-
l_whence:要加锁或解锁的区域的字节偏移量,与l_start共同决定
-
l_len:区域的字节长度
-
l_pid:持有该锁的进程ID
l_start是相对偏移量,l_whence决定了相对偏移量的起点,可以取:SEEK_SET、SEEK_CUR、SEEK_END三个值中的一个。该区域可以在当前文件尾端处开始或越过其尾端处开始,但是不能在文件的其实位置之前开始。若l_len为0,则意为从起点开始直到最大可能偏移量为止,无论在该文件中写入多少数据,都处于锁的范围内。

fcntl的三种命令:
F_GETLK :判断由flockptr所描述的锁是否会被另一把锁阻塞。若被阻塞,则将该锁的信息写道flockptr指向的结构中。如果不存在这种情况,则除了l_type被设置为F_RDLCK之外,flockptr指向的结果中的其他信息保持不变
F_SETLK :设置由flockptr所描述的锁,若失败,则fcntl立即出错返回,errno被设置为EACCES或EAGAIN
F_SETLKW :与F_SETLK功能相似,只不过如果建立锁失败,则阻塞,调用进程休眠,直到该创建锁的请求被允许,使用信号唤醒进程,建立锁
系统会按需求组合或开裂相邻区,假如100-199字节上锁,需要对第150字节解锁,则上锁区被分为两段,系统将创建两个锁分别进行管理:

若再次对150字节加锁,则几个连续的加锁区又会合并,恢复到初始的状态。
子进程不能继承父进程持有的锁。在程序执行exec之后,新程序可以继承原执行程序的锁。但是,若对ige文件描述符设置了close-on-exec标志,那么当作为exec的一部分关闭该文件描述符时,对相应文件的所有锁都被释放了
建议性锁和强制性锁:
什么是建议性锁和强制性锁:建议性锁,内核对其只提供查询的接口,程序准备访问某资源时,可以使用该接口查询锁的状态,但是内核并不阻止程序访问资源,也就是说程序必须严格遵守规定,在确认该资源无锁时才能访问,否则即使该资源被上锁,进程仍然可强行访问该资源;相对的,强制性锁意味着内核直接干预程序对上锁资源的操作,任何违反规定的操作都会被阻塞或者直接失败。强制性锁意味着更健壮的程序设计,但也意味着更大的IO开销,一般的数据库等程序一般都使用建议性锁。
三、IO多路转接
有如下场景,当我们使用telnet命令远程登录一个终端时,此程序需要进行以下处理:1.从用户的终端读入信息 2.将用户的信息通过网络通信发送给本地主机,主机上的telnet守护进程接收信息,将信息写入本地shell 3.接收shell输出的数据 4. 将shell输出的数据输出到用户终端。这样用户使用telnet,能够远程登录一个shell,仿佛这个shell是在本地工作一样。对于telnet,工作流程如下图:

对于telnet命令,时刻需要从两个文件描述符中读取有效数据,如何能够及时的读取两个文件描述符发送的信息,是我们要讨论的问题。有如下方法:
- 最简单的方法,阻塞式的等待两个文件描述符,直到两个文件描述符都传递来有效数据。这样的缺点很明显:如果只有一个文件描述符传递来有效数据,另一个没有,则这个有效数据也会被一直阻塞
2.创建两个进程或两个线程,分别等待两个文件描述符。这样做会导致进程或线程的同步十分困难,当任务结束时,两个进程或线程需要使用信号等复杂机制来确保同时结束
3.设置读取为非阻塞式读取,然后对两个文件描述符进行轮询,先访问第一个,若出错返回,则立即访问第二个,若还出错,则再访问第一个,直到读取到有效数据。这样做会大大浪费CPU资源。
4.使用异步IO技术,只有当文件描述符传递来有效数据时才进行处理,这是理想状态,但并不是每个系统都支持异步IO,并且如果在等待多个文件描述符,异步IO之会发送同一种信号给进程,进程无法据此分析是哪一个文件描述符传递来了有效的数据,仍然需要对所有文件描述符进行轮询。
以上四种方法都有者各自的缺陷,我们可以使用IO多路转接技术完成此项任务。这种技术先创建一张有关描述符的列表,然后调用一个函数,直到这些描述符中的一个已经准备好时,该函数才返回,并且告知进程是哪个文件描述符准备好了。
poll、pselect和select这三个函数使我们能使用IO多路转接

在所有依从POSIX标准的系统中,都可以使用select,我们需要输入参数告知select函数的信息:
1.我们所关心的描述符
2.对于每个描述符我们所关心的状态(读、写、或者是描述符异常)
3.愿意等待多长时间
select返回:
1.已经准备好的描述符数量
2.对于读、写、异常这三种状态的每一个,哪几个文件描述符准备好了
cpp
#include <sys/select.h>
int select(int maxfdp1,fd_set *restrict readfds,fd_set *restrict writefds,
fd_set *restrict execptfds,struct timeval *restrict tvptr);
1.tvptr用来指定愿意等待的时间:

tvptr==null:永远等待,直到文件描述符准备好或者捕捉到一个信号
tvptr中的两个时间参数==0:完全不等待
tvptr中的两个时间参数> 0:等待指定的时间
2.readfds、writefds、execptfds是指向描述符集的指针,fd_set这种数据类型为每一种可能的描述符保持了一位,如下:

fd_set是一个位数组,可以使用以下宏进行操作:
-
FD_ZERO(fd_set *set) :清空集合
-
FD_SET(int fd,fd_set *set) :将fd加入集合
-
FD_CLR(int fd,fd_set *set) :从集合中移除fd
-
FD_ISSET(int fd,fd_set *set) :检查fd是否在集合中
3.select有三个可能的返回值:
-
返回-1,表示出错,比如当所指定的描述符都没有准备好时捕捉到一个信号。
-
返回0,表示没有描述符准备好,当时间超出等待时间且描述符都没准备好时返回。
-
返回正数,表示已经准备好的描述符数总和,假如一个描述符准备好被读和被写,则相应的计数+2
如果在一个描述符上碰到结尾处,则select认为该描述符"可读",而不是"异常"。
poll函数类似于select函数,不同的是,poll为每个元素分配一个结构体,而不是使用数组的方法:
cpp
#include <poll.h>
int poll(struct pollfd fdarray[],nfds_t nfds,int timeout);
struct pollfd{
int fd;
short events;
short revents;
}
每个元素的event成员告知内核我们关心的是什么,返回时内核设置revent,event取值如下表:

timeout参数表示我们愿意等待的时间。
书中只介绍了select、pselect和poll函数,笔者这里为大家补充一个Linux下常用到的多路转接函数:epoll,相当于select和poll的增强版:
cppint epoll_create(int size);//创建一个epoll示例,返回一个文件描述符,后续操作均基于此句柄 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); //向epoll实例中增加、修改、删除一个元素,此元素包括一个文件描述符以及想要监听的事件。 //op:EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(删除) //event:指定要监视的事件类型(如 EPOLLIN 可读、EPOLLOUT 可写)及用户数据 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); //等待事件发送 //events:用于接收事件的数组 //timeout:等待时间epoll支持两种触发方式:
1.水平触发:只要文件描述符处于就绪状态,每次epoll_wait都会返回,直到数据被读取
2.边缘触发:只有文件描述符状态改变的时候才会使wait函数返回
epoll的优点是它每次只返回状态变化或就绪的文件描述符,而不是像select/poll一样返回所有元素,我们不需要遍历数组来查看哪个文件描述符是就绪的,时间复杂度从o(n)变为o(1),更加高效;支持两种触发方式,便于操作
四、readv和writev函数
readv和writev函数用于一次性读取多个非连续的缓冲区,称为分布读 和聚集写
cpp
#include <sys/uio.h>
ssize_t readv(int filedes,const struct iovec *iov,int iovcnt);
ssize_t writev(int filedes,const struct iovec *iov,int iovcnt);
//成功则返回已读、已写的字节数,出错则返回-1
参数中的iovec结构:
struct iovec{
void *iov_base;
size_t iov_len;
}
参数iovcnt是iov中的元素个数,也就是缓冲区个数。结构如下所示:

分布读readv:从文件描述符fd中读取数据,并输出到列表中的几个缓冲区;
聚集写writev:从几个分散的缓冲区读取数据,输出到文件描述符fd中;
五、readn和writen函数
在对某些特殊设备如管道、STREAM、网络设备等等进行读取时,可能实际读到的字节数小于要求读到的字节数,这不是错误,而是预期中的结果,应该继续读取直到读取足够要求的字节数;同样,向这些设备写入时,有可能实际写入的字节数小于要求写入的字节数,可能是因为网络阻塞原因等等,应该继续写入直到写入所有数据。我们可以使用readn和writen函数解决这些问题,这两个函数可以读写指定的N个字节,它们会多次调用read和write直至读取了N个字节为止:
cpp
ssize_t readn(int filedes,void *buf,size_t nbytes);
ssize_t writen(int filedes,void *buf,size_t nbytes);
六、存储映射IO
使一个磁盘文件映射到一块储存空间的缓冲区,这样,读取这个缓冲区的数据就相当于从文件中读取数据;向此缓冲区中写入数据就相当于向文件中写入数据,这样可以不使用read和write的情况下进行IO。将一个给定的文件映射到一个存储区域:
cpp
#include <sys/mman.h>
void *mmap(void *addr,size_t len,int prot,int flag,int filedes,off_t off);
//若成功则返回映射区的起始地址,出错则返回MAP_FAILED
-
addr指定映射区的起始地址,通常将其设置为0,表示又系统自动分配映射区地址,起始地址作为函数返回值返回。
-
filedes是被映射的文件,在映射操作之前需要先打开文件,lens表示文件长度,off表示映射字节在文件中的起始偏移量
-
prot参数说明对映射区的保护要求(可使用按位或组合使用):
(1)PROT_READ 映射区可读
(2)PROT_WRITE 映射区可写
(3)PROT_EXEC 映射区可执行
(4)PROT_NONE 映射区不可访问
- flag参数影响映射存储区的多种属性:
(1)MAP_FIXED:返回值必须等于addr。如果未设置此标志,内核只会将非0的addr入参作为一种参考建议,而不是绝对的遵从和执行
(2)MAP_PRIVATE:本标志说明对映射区的存储操作导致创建该映射文件的一个私有副本。所有后来对该映射区的引用都是引用该副本,而不是引用原始文件

与映射存储区相关的信号有:SIGSEGV和SIGBUS,SIGEGV通常用于指示进程试图访问对它不可用的存储区,假如进程试图访问只读的映射存储区,则会收到SIGSEGV;如果访问一个映射存储区中已经不存在的部分(比如文件被截短),则会收到SIGBUS。
调用fork之后,子进程继承父进程的映射存储区,一旦调用exec,则不会再继承此存储区。
调用mprotect可以更改一个现存映射区的权限:
cpp
#include <sys/mman.h>
int mprotect(void *addr , size_t len , int prot);
如果在共享存储映射区中的页已被修改,那么可以调用msync将该页冲洗到被映射的文件中(类似于fsync):
cpp
#include <sys/mman.h>
int msync(void *addr,size_t len,int flags);
进程终止时,或调用munmap之后,存储映射区就被自动解除映射。需要注意的是,关闭文件描述符并不解除映射区。调用munmap不会影响被映射的对象(也不会触发被映射对象的更新):
cpp
#include <sys/mman.h>
int munmap(caddr_t addr,size_t len);