在最前面,咱们讲过IO分红带缓冲的IO和不带缓冲的IO,可是实际上,这个区别并非很大,由于缓冲区并无影响到实际的读写。咱们知道,系统调用实际上分红两种,高速的系统调用和低速的系统调用,换句话说,低速的调用会致使系统永久性阻塞,可是须要注意的是,并非磁盘IO都是低速调用。好比open、read、write函数,若是这些操做不能完成就会马上出错返回,并不会致使系统阻塞。在前面的时候咱们也学到过,若是在open的时刻,指定O_NONBLOCK
,或者在一个已打开的文件描述符上调用fcntl函数,附加上O_NONBLOCK
参数。实际上虽然指定了参数,可是在某些状况下颇有可能丢失信息。在大量传输信息的时候容易出现系统调用大量失败的状况。javascript
在不少状况下,咱们须要面对多方一块儿操做文件的状况,这就是一个典型的资源竞争冲突,为了保证文件的正确读写,Unix系统提供了文件记录锁的机制,也就是上文中提到过的文件记录锁。为了提供这个功能,各个系统都自行实现了API,其中,POSIX1.x标准规定的是fcntl方法,而BSD系列则是规定flock方法,SystemV在fcntl方法的基础上构建了lockf函数java
int fcntl(int fildes, int cmd, ...);
The commands available for advisory record locking are as follows:
F_GETLK Get the first lock that blocks the lock description pointed to by the third argument, arg, taken as a pointer to a struct flock (see above). The information retrieved overwrites the information passed to fcntl in the flock structure. If no lock is found that would prevent this lock from being created, the structure is left unchanged by this function call except for the lock type which is set to F_UNLCK. F_SETLK Set or clear a file segment lock according to the lock description pointed to by the third argument, arg, taken as a pointer to a struct flock (see above). F_SETLK is used to establish shared (or read) locks (F_RDLCK) or exclusive (or write) locks, (F_WRLCK), as well as remove either type of lock (F_UNLCK). If a shared or exclusive lock cannot be set, fcntl returns immediately with EAGAIN. F_SETLKW This command is the same as F_SETLK except that if a shared or exclusive lock is blocked by other locks, the process waits until the request can be satisfied. If a signal that is to be caught is received while fcntl is waiting for a region, the fcntl will be interrupted if the signal han-dler has not specified the SA_RESTART (see sigaction(2)).复制代码
前面也介绍过这个函数,不过此次会讲解记录锁的内容,对于记录所来讲,cmd参数是F_GETLK
、F_SETLK
或者FSETLKW
,第三个参数是一个纸箱flock结构体的指针数据库
struct flock {
off_t l_start; /* starting offset */
off_t l_len; /* len = 0 means until end of file */
pid_t l_pid; /* lock owner */
short l_type; /* lock type: read/write, etc. */
short l_whence; /* type of l_start */
};复制代码
基本上也不用讲解了,注释早已说明一切。这个结构体就是经过指定文件区域和锁的类型等参数锁定文件。不过须要注意的是,l_type其实是取值SEEK_SET
、SEEK_CUR
、或SEEK_END
。而且上面提到的类型只有两种:共享读锁和独占写锁,实际上就是读写锁。编程
F_GETLK
参数判断flockptr参数所描述的锁是否会被另外一把锁排斥F_SETLK
参数设置由flockptr所描述的锁F_SETLKW
这是F_SETLK的阻塞版本很容易想到,在开发中确定是先用F_GETLK参数测试是否能创建一把锁,然后使用F_SETLK或者F_SETLKW创建锁,可是这二者并非原子操做,前面已经讲过,非原子操做很容易致使操做冲突。
在设置释放锁的时候,内核是根据字节数维持锁的范围的,也就是说,实际上内核只是维护了一个flock结构体的链表,而后每次的锁更改都会致使链表被遍历而且合并。
对于记录锁的自动继承和释放有3条规则:数组
其实锁对数据库这种大量读写IO的程序才是最有用的,因此基本上锁就能够直接考虑数据库的环境,若是数据库的客户端库使用的是同一套锁机制,那就能保证文件的共享访问,可是建议性锁没法保证其余有权限存取数据库文件的进程读写此文件。而强制性锁则会让进程检查每个open、read和write函数,验证调用进程是否违背了正在访问文件的锁,这就是强制性锁和建议性锁的区别。网络
前面谈到过,对于内核来讲,IO只有两种方式:阻塞和非阻塞,阻塞IO会致使CPU等待IO从而浪费等待时间,因此系统提供了非阻塞IO,可是非阻塞IO带来的问题就是完整IO没有完成,为了获取完整的数据,应用程序须要重复调用IO操做来确认是否完成,也就是轮询。
当从一个文件描述符读,而后又写到另外一个描述符时,一般会写出如下代码数据结构
while ((n = read(STDIN_FILENO, buf, BUFSIZE)) > 0)
if (write(STDOUT_FILENO, buf, n) != n)
err_sys("write error");复制代码
这种循环获取的形式就是轮询,很是简单,可是消耗了CPU资源,而且若是须要有更高的要求,好比必须从两个文件描述符读取。
典型的应用就是网络守护进程,例如Nginx和Telnet,这里直接拿原著中的Telnet讲解,telnet因为存在两个输入两个输出,因此不能使用阻塞式的IO函数,开发者的第一反应,应该是fork函数,使用两个进程,每一个进程都负责一条读写通道,可是这就须要进程同步,而多线程编程也一样是这样的问题。
另外一个方法就是使用一个进程,可是使用非阻塞IO读取数据。其基本思想很简单,两个描述符都读取,可是一直处于循环,每次循环都查询一次两个文件描述符,若是没有就马上返回不阻塞,这种循环就是典型的轮询,这是种很是常见的技术,实际上倒是很是浪费CPU资源的技术,因此目前,基本开发以及不能也不推荐了。
还有几种技术就是异步IO,这种技术实质上就是相似通知,当描述符准备完毕后,进程通知内核,可是实际上目前原生API并不能作到移植,因此,目前大部分的开发,包括Node.js等在内的网络服务,基本都是使用第三方或者本身实现线程池。不过,目前Linux系统已经有了名为AIO的原生异步IO。
如今目前大部分的使用方式就是IO多路转接,系统构造一张链表,里面存储全部的文件描述符,而后调用函数侦听,知道其中一个已经准备完毕的时候返回。poll、pselect和select三个函数就是这样执行的。多线程
这连个函数是POSIX规定的app
int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);
int pselect(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, const struct timespec *restrict timeout, const sigset_t *restrict sigmask);复制代码
第一个参数nfds的意思就是“最大文件描述符编号值+1”,由于文件描述符都是从0开始的,从后面readfds、writefds、errorfds中找出最大描述符编号值并+1就是这个参数的值,中间三个参数是指向描述符集的指针,使用fd_set
数据结构表示,实际上有下列五个函数异步
void FD_CLR(fd, fd_set *fdset);
void FD_COPY(fd_set *fdset_orig, fd_set *fdset_copy);
void FD_ISSET(fd, fd_set *fdset);
void FD_SET(fd, fd_set *fdset);
void FD_ZERO(fd_set *fdset);复制代码
是否是发现比原著多了一个FD_COPY函数,实际上就是复制用的,可有可无。最后一个参数就是制定愿意等待的时间长度,使用timeval结构体,也就是能够指定秒和微妙单位。
select实际上和描述符自己阻塞无关,它只是简化了咱们监听一堆文件描述符的繁琐操做,除了select之外,上面还有一个select的变体pselect,pselect和select很像,可是select得超时值用timeval结构体定义,pselect使用timespec结构,pselect可以使用可选信号屏蔽字,若是sigmask为null,则二者同样,可是sigmask指向屏蔽字的时候,将以原子操做形式安装屏蔽字。
除了select之外,你们应该还见过poll函数
int poll(struct pollfd fds[], nfds_t nfds, int timeout);复制代码
看起来poll函数相对于select更加简洁易懂,select函数对三种类型都指定了参数用于构造描述符集,可是poll函数使用的则是pollfd结构体数组,pollfd结构体以下
struct pollfd {
int fd; /* file descriptor */
short events; /* events to look for */
short revents; /* events returned */
};复制代码
nfds参数指定了fds数组的大小,从上面的注释中应该也看得出来结构体到底是怎么构造的,events是咱们关心fd的事件,而revents则是内核设置,返回的时候用于说明每一个描述符发生了哪些事件。
The event bitmasks in events and revents have the following bits:
POLLERR An exceptional condition has occurred on the device or socket. This flag is output only, and ignored if present in the input events bitmask.
POLLHUP The device or socket has been disconnected. This flag is output only, and ignored if present in the input events bitmask. Note that POLLHUP
and POLLOUT are mutually exclusive and should never be present in the revents bitmask at the same time.
POLLIN Data other than high priority data may be read without blocking. This is equivalent to ( POLLRDNORM | POLLRDBAND ).
POLLNVAL The file descriptor is not open. This flag is output only, and ignored if present in the input events bitmask.
POLLOUT Normal data may be written without blocking. This is equivalent to POLLWRNORM.
POLLPRI High priority data may be read without blocking.
POLLRDBAND Priority data may be read without blocking.
POLLRDNORM Normal data may be read without blocking.
POLLWRBAND Priority data may be written without blocking.
POLLWRNORM Normal data may be written without blocking.复制代码
上面是两个参数可取的值,每一个系统实现可能存在误差,因此须要自行尝试。
前面讲过,非阻塞IO带来的就是轮询,前面内容包括前面的章节整合一下,能够概括出如下主流轮询技术:
虽然轮询知足了非阻塞IO获取完整数据的需求,可是依旧是同步的,也须要花费CPU用于便利文件描述符或者休眠等待事件发生。因此就有了异步IO,目前据笔者所知,只有Linux下有AIO技术算是真正原生提供的API。
可是,实际上,是有模拟方式的,信号机构提供了异步形式通知事件发生的方法,使用一个信号通知进程,可是,因为信号是有限的,若是使用一个信号,则进程不知道是哪一个文件描述符发生的事件,若是用多个信号,文件描述符的数量可能远远超出信号的数量。
实际上,最容易想到的办法就是多线程。让部分线程进行阻塞IO或者非阻塞IO加轮询技术来完成数据获取,让另外一个线程进行计算,然后经过线程间通讯将IO获得的数据进行传递,就能轻松实现异步IO。
SystemV中异步IO是归属给STREAMS系统的,他只能用于STREAMS设备和管道,异步IO信号是SIGPOLL。实际上因为这种机制自己的限制,目前已经找不到Unix环境会去采用它了,因此这里也不须要再讲解了。
对于BSD系列的系统来讲,异步IO信号是SIGIO和SIGURG信号的组合,SIGIO是通用异步IO的信号,SIGURG则是通知网络链接的数据已经到达。
POSIX标准对不一样类型文件异步IO提供了可移植的模型,异步IO使用AIO控制块来描述IO操做。
struct aiocb {
int aio_fildes; /* File descriptor */
off_t aio_offset; /* File offset */
volatile void *aio_buf; /* Location of buffer */
size_t aio_nbytes; /* Length of transfer */
int aio_reqprio; /* Request priority offset */
struct sigevent aio_sigevent; /* Signal number and value */
int aio_lio_opcode; /* Operation to be performed */
};复制代码
上面是苹果系统下的AIO控制块实现,实际上和POSIX规定几乎同样,它是继承于FreeBSD3.0的AIO实现,
从上面能够看出,每一个字段究竟的意义,aio_fildes就是文件描述符,读写操做从aio_offset指定的偏移量位置开始,对于读操做,会将数据复制到aio_buf的缓冲区内,对于写操做,会从这个缓冲区写入磁盘,aio_nbytes字段指定了读写的字节数。
除了上面4个字段之外,aio_reqprio就是异步IO请求的顺序,aio_sigevent就是IO事件完成后如何通知,而aio_lio_opcode就是执行的操做。
struct sigevent {
int sigev_notify; /* Notification type */
int sigev_signo; /* Signal number */
union sigval sigev_value; /* Signal value */
void (*sigev_notify_function)(union sigval); /* Notification function */
pthread_attr_t *sigev_notify_attributes; /* Notification attributes */
};复制代码
sigevent结构体是归属于signal信号机制模型中的数据结构,其中sigev_notify字段是通知类型
int aio_read(struct aiocb *aiocbp);
int aio_write(struct aiocb *aiocbp);复制代码
在异步IO以前须要先初始化AIO控制块,当函数返回成功时候,异步IO请求就已经被放在了等待处理队列中。这些返回值与实际IO擦作的结果没有任何关系,若是想要强制全部等待中的异步操做不等待直接写入存储,则调用aio_fsync函数
固然,好像aio_fsync函数并非很是普遍,因此在使用的时候记得运行时检查。
为了获取一个异步读写的完成状态,能够调用aio_error
函数
int aio_error(const struct aiocb *aiocbp);复制代码
返回以下:
ssize_t aio_return(struct aiocb *aiocbp);复制代码
记住在aio_error检查已经成功以前,不要调用aio_return函数,并且须要小心每一个异步操做只能调用一次aio_return函数。
若是在其余操做完成以后,异步操做还未完成,那可使用
int aio_suspend(const struct aiocb *const list[], int nent, const struct timespec *timeout);复制代码
aio_suspend函数会阻塞当前进程直到操做完成,通常状况下不多会使用。
若是咱们想要取消已经处于进行中的异步操做,可使用以下函数
int aio_cancel(int fildes, struct aiocb *aiocbp);复制代码
这个函数会返回4个返回值:
除了上述函数之外,还有一个函数也被包含在异步请求函数中,可是实际上不多见到,因此这里就很少作讲解。
ssize_t readv(int d, const struct iovec *iov, int iovcnt);
ssize_t writev(int fildes, const struct iovec *iov, int iovcnt);复制代码
这两个函数用于在一次读写中读写多个非连续的缓冲区,也就是说能够将传统的多个函数读写调用压缩到一个,这连个函数第二个参数就是一个指向iovec结构体的指针,其实是一个指向数组的指针
struct iovec {
char *iov_base; /* Base address. */
size_t iov_len; /* Length. */
};复制代码
第三个参数就是数组的长度。iov数组中的元素最大值就是IOV_MAX。
存储映射IO能将一个磁盘文件映射到存储空间中的一个缓冲区上,因而,当从缓冲区中读取数据的时候,就等同于读取文件。Unix系统提供了此类函数
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);复制代码
addr指定映射存储区的起始地址。一般为0,也就是系统自动分配区域。fd参数指定被映射文件的文件描述符,也就表明必须先打开这个文件。prot参数指定了映射存储区的保护要求以下:
|prot|说明|
|----|---|
|PROT_READ|存储区可读|
|PROT_WRITE|存储区可写|
|PROT_EXEC|存储区可执行|
|PROT_NONE|存储区不可访问|
固然,这个参数的指定必然是基于文件描述符的打开方式的,很容易明白,由于存储映射IO技术本质上仍是基于文件描述符的,因此不可能绕过文件描述符的限制读写。
flag参数影响映射存储区的多种属性,以下就是可选值:
MAP_ANONYMOUS Synonym for MAP_ANON.
MAP_ANON Map anonymous memory not associated with any specific file. The offset argument is ignored. Mac OS X specific: the file descriptor used
for creating MAP_ANON regions can be used to pass some Mach VM flags, and can be specified as -1 if no such flags are associated with the
region. Mach VM flags are defined in <mach/vm_statistics.h> and the ones that currently apply to mmap are:
VM_FLAGS_PURGABLE to create Mach purgable (i.e. volatile) memory
VM_MAKE_TAG(tag) to associate an 8-bit tag with the region
<mach/vm_statistics.h> defines some preset tags (with a VM_MEMORY_ prefix). Users are encouraged to use tags between 240 and 255. Tags
are used by tools such as vmmap(1) to help identify specific memory regions.
VM_FLAGS_SUPERPAGE_SIZE_* to use superpages for the allocation. See <mach/vm_statistics.h> for supported architectures and sizes (or
use VM_FLAGS_SUPERPAGE_SIZE_ANY to have the kernel choose a size). The specified size must be divisible by the superpage size (except for
VM_FLAGS_SUPERPAGE_SIZE_ANY), and if you use MAP_FIXED, the specified address must be properly aligned. If the system cannot satisfy the
request with superpages, the call will fail. Note that currently, superpages are always wired and not inherited by children of the process.
MAP_FILE Mapped from a regular file. (This is the default mapping type, and need not be specified.)
MAP_FIXED Do not permit the system to select a different address than the one specified. If the specified address cannot be used, mmap() will fail.
If MAP_FIXED is specified, addr must be a multiple of the pagesize. If a MAP_FIXED request is successful, the mapping established by
mmap() replaces any previous mappings for the process' pages in the range from addr to addr + len. Use of this option is discouraged.
MAP_HASSEMAPHORE Notify the kernel that the region may contain semaphores and that special handling may be necessary.
MAP_PRIVATE Modifications are private (copy-on-write).
MAP_SHARED Modifications are shared.
MAP_NOCACHE Pages in this mapping are not retained in the kernel's memory cache. If the system runs low on memory, pages in MAP_NOCACHE mappings will
be among the first to be reclaimed. This flag is intended for mappings that have little locality and provides a hint to the kernel that
pages in this mapping are unlikely to be needed again in the near future.复制代码
这就不讲解了,原著上已经讲解的足够清楚了。
调用mprotect能够更改现有映射的权限
int mprotect(void *addr, size_t len, int prot);复制代码
也就是一个修改映射区域权限的函数,当页已经修改完毕,能够调用msync函数冲洗到被映射的文件中。
int msync(void *addr, size_t len, int flags);复制代码
基本就和fsync函数差很少,也很少说了,基本上都在Unix手册上
当进程终止的以后,天然会自动解除存储区的映射,或者能够调用munmap函数解除
int munmap(void *addr, size_t len);复制代码
munmap函数删除了指定地址的映射,若是继续对其进行读写会致使无效内存引用。而且这个函数不会冲洗缓冲区内容到文件,因此须要当心使用。