APUE - 高级 I/O

非阻塞 I/O

对于一个给定的描述符有两种办法对其指定非阻塞 I/O:

  1. 如果调用 open 获得描述符,则可指定 O_NONBLOCK 标志。
  2. 对于已经打开的一个描述符,则可调用 fcntl,由该函数打开 O_NONBLOCK 文件状态标志。

记录锁

记录锁的功能是:当一个进程正在读或修改文件的某个部分时,它可以阻止其他进程修改同一文件区。

fcntl记录锁

#include <fcntl.h>
int fcntl(int filedes, int cmd, ... /* struct flock *flockptr */ );

struct flock {
    short   l_type;     /* F_RDLCK, F_WRLCK, or F_UNLCK */
    off_t   l_start;    /* offset in bytes, relative to l_whence */
    short   l_whence;   /* SEEK_SET, SEEK_CUR, or SEEK_END */
    off_t   l_len;      /* length, in bytes; 0 means lock to EOF */
    pid_t   l_pid;      /* returned with F_GETLK */
};

对于记录锁,cmd 是 F_GETLK、F_SETLK 或 F_SETLKW。

多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定字节上只能有一个进程独用的一把写锁。进一步而言,如果在一个给定字节上已经有一把或多把读锁,则不能在该字节上再加写锁;如果在一个字节上已经有一把独占性的写锁,则不能再对它加任何读锁。

如果一个进程对一个文件区间已经有了一把锁,后来该进程又企图在同一文件区间再加一把锁,那么新锁将替换老锁。

加读锁时,该描述符必须是读打开;加写锁时,该描述符必须是写打开。

锁的隐含继承和释放

关于记录锁的自动继承和释放有三条规则:

  1. 锁与进程和文件两方面有关。这有两重含义:当一个进程终止时,它所建立的锁全部释放;任何时候关闭一个描述符时,则该进程通过这一描述符可以引用的文件上的任何一把锁都被释放(这些锁都是该进程设置的)。
  2. 由 fork 产生的子进程不继承父进程所设置的锁。
  3. 在执行 exec 后,新程序可以继承原执行程序的锁。但是注意,如果对一个文件描述符设置了 close-on-exec 标志,那么当作为 exec 的一部分关闭该文件描述符时,对相应文件的所有锁都被释放了。

I/O多路转接

select 和 pselect 函数

#include <sys/select.h>
int select(int maxfdp1, fd_set *restrict readfds,
           fd_set *restrict writefds, fd_set *restrict exceptfds,
           struct timeval *restrict tvptr);

struct timeval {
    long    tv_sec;     /* seconds */
    long    tv_usec;    /* and microseconds */
};

#include <sys/select.h>
int pselect(int maxfdp1, fd_set *restrict readfds,
            fd_set *restrict writefds, fd_set *restrict exceptfds,
            const struct timespec *restrict tsptr,
            const sigset_t *restrict sigmask);

poll 函数

#include <poll.h>
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);

struct pollfd {
    int     fd;         /* file descriptor to check, or <0 to ignore */
    short   events;     /* events of interest on fd */
    short   revents;    /* events that occurred on fd */
};

异步 I/O

SysV 异步 I/O

在 SysV 中,异步 I/O 是 STREAMS 系统的一部分。它只对 STREAMS 设备和 STREAMS 管道起作用。SysV 的异步 I/O 信号是 SIGPOLL。

BSD 异步 I/O

在 BSD 派生的系统中,异步 I/O 是 SIGIO 和 SIGURG 两个信号的组合。前者是通用异步 I/O 信号,后者则只用来通知进程在网络连接上到达了带外的数据。

为了接收 SIGIO 信号,需执行下列三步:

  1. 调用 signal 或 sigaction 为 SIGIO 信号建立信号处理程序。
  2. 以命令 F_SETOWN 调用 fcntl 设置进程 ID 和进程组 ID,它们将接收对于该描述符的信号。
  3. 以命令 F_SETFL 调用 fcntl 设置 O_ASYNC 文件状态标志,使在该描述符上可以进行异步 I/O。

第 3 步仅能对指向终端或网络的描述符执行,这是 BSD 异步 I/O 设施的一个基本限制。

对于 SIGURG 信号,只需执行第 1 步和第 2 步。该信号仅对引用支持带外数据的网络连接描述符而产生。

readv 和 writev 函数

readv 和 writev 函数用于在一次函数调用中读、写多个非连续缓冲区。

#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);

struct iovec {
    void   *iov_base;   /* starting address of buffer */
    size_t  iov_len;    /* size of buffer */
};

writev 以顺序 iov[0],iov[1] 至 iov[iovcnt-1] 从缓冲区中聚集输出数据。writev 返回输出的字节总数,通常,它应等于所有缓冲区长度之和。

readv 则将读入的数据按上述同样顺序散布到缓冲区中。readv 总是先填满一个缓冲区,然后再填写下一个。readv 返回读到的总字节数。如果遇到文件结尾,已无数据可读,则返回 0。

存储映射 I/O

存储映射 I/O 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,则相应字节就自动地写入文件。

#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flag, int filedes, off_t off);

与映射存储区相关的有 SIGSEGV 和 SIGBUS 两个信号。信号 SIGSEGV 通常用于指示进程试图访问对它不可用的存储区。如果进程企图存数据到 mmap 指定为只读的映射存储区,那么也产生此信号。如果访问映射区的某个部分,而在访问时这一部分实际上已不存在,则产生 SIGBUS 信号。

在调用 fork 之后,子进程继承存储映射区,调用 exec 后的新程序则不继承此存储映射区。

调用 mprotect 可以更改一个现存映射存储区的权限。

#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);

如果在共享存储映射区中的页已被修改,那么我们可以调用 msync 函数将该页冲洗到被映射的文件中。

#include <sys/mman.h>
int msync(void *addr, size_t len, int flags);

进程终止时,或调用了 munmap 之后,存储映射区就被自动解除映射。关闭文件描述符 filedes 并不解除映射区。

#include <sys/mman.h>
int munmap(caddr_t addr, size_t len);

调用 munmap 不会使映射区的内容写到磁盘文件上。