Linux IO操作

除了最基本的读写操作之外,Linux还提供了一些高级特征。非阻塞的IO操作允许在文件未准备好的情况下直接返回错误。记录锁让文件的一个区域锁住,防止被其它人篡改。 IO多路复用允许我们同时查询多个文件中哪些已经准备好。异步IO则可以通过异步IO控制块进行异步传输。分散聚集读写操作允许我们将连续的读写操作分割成多个读写操作同时发送出去。固定长度读写让我们能够准确读取指定数目的长度,这实际上是多次调用读写来实现。内存映射可以让我们像访问内存一样的方式来修改文件。

非阻塞IO操作

非阻塞IO就是在我们读写数据的时候,发送命令后就立即返回,如果返回错误码,就表示尚需等待。有两种方式可以让我们实现非阻塞的IO操作。

  • O_NONBLOCK标志调用open函数
  • 用fcntl函数打开O_NONBLOCK标志

记录锁

当两个人同时编辑一个文件的时候,后编辑的将覆盖先编辑的,为了避免这种问题,记录锁可以保证进程单独占用文件一段区域。实际上是通过fcntl函数实现。

int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );
struct flock {
    short l_type;                       // F_RDLCK, F_WRLCK, or F_UNLCK
    short l_whence;                     // SEEK_SET, SEEK_CUR, or SEEK_END
    off_t l_start;                      // offset in bytes, relative to l_whence
    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等值,最后一个F_SETLKW是阻塞形式,表示Wait。

记录锁关联一个进程和一个文件

fd1 = open(pathname, ...);
read_lock(fd1, ...);
fd2 = dup(fd1);
close(fd2);                             // lock on fd1 is released
fd1 = open(pathname, ...);
read_lock(fd1, ...);
fd2 = open(pathname, ...)
close(fd2);                             // lock on fd1 is released

子进程不继承记录锁,所以fork的子进程需要自己去获取。但是exec产生的进程则会继承。

IO复用

while ((n = read(STDIN_FILENO, buf, BUFSIZ)) > 0)
    if (write(STDOUT_FILENO, buf, n) != n)
        err_sys("write error");

我们经常可以看到这样的代码,但是如果我们企图从两个文件读取,就不能简单在一个文件上面阻塞了。当然我们可以产生两个线程,这样当一个文件结束的时候我们就需要发送信号通知另一个线程,这就显得比较复杂了。

另外一种方法是我们用非阻塞的形式,在每个文件上等待一会,这样的方式叫轮询,比较费CPU。轮询的方式不适合多任务系统。

还有一种方法叫异步IO,当文件准备好的时候,内核给我们发送一个信号来通知我们。这样做面临一个移植性问题,主要是各种规范定义的接口不一致。另外一个问题就是当信号产生的时候,我们不知道是哪个文件已经准备好。

IO复用技术用于解决这类问题,主要涉及三个函数,poll、pselect和select。函数select告诉内核:

  • 对哪个文件描述符感兴趣
  • 需要等待何种状态
  • 想要等待多长时间

然后内核会告诉我们:

  • 总共可以使用的文件描述符个数
  • 哪些描述符已经准备好
int select(int maxfdp1, fd_set *restrict readfds,
           fd_set *restrict writefds, fd_set *restrict exceptfds,
           struct timeval *restrict tvptr);
tvptr
如果传递NULL表示将会等到至少有一个文件描述符准备好

另外提供了一组宏来操作fd_set,如下所示。

int FD_ISSET(int fd, fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_ZERO(fd_set *fdset);
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);

和select相比,该函数有如下不同。

  • 时间采用timespec,精度为纳秒级别
  • 如果sigmask为NULL,在处理信号上和select行为一致,如果指定了sigmask,在执行pselect期间自动安装信号mask,当执行完成之后又会恢复原来的状态
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
};
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);

异步IO

使用select和poll实现的是同步通知模型,使用信号可以异步通知我们感兴趣的文件发生了什么。但是异步IO也有局限性,如果是关注多个文件,就无法确定是哪个文件产生的变化。 POSIX异步IO利用AIO控制块描述IO操作。

struct aiocb {
    int aio_fildes;                     // file descriptor
    off_t aio_offset;                   // file offset for I/O
    volatile void *aio_buf;             // buffer for I/O
    size_t aio_nbytes;                  // number of bytes to transfer
    int aio_reqprio;                    // priority
    struct sigevent aio_sigevent;       // signal information
    int aio_lio_opcode;                 // operation for list I/O
};
struct sigevent {
    int sigev_notify;
    int sigev_signo;
    union sigval sigev_value;
    void (*sigev_notify_function)(union sigval);
    pthread_attr_t *sigev_notify_attributes;
};

要执行异步IO,只需要将构造好的控制块传递给读写命令即可。

int aio_read(struct aiocb *aiocb);
int aio_write(struct aiocb *aiocb);
int aio_fsync(int op, struct aiocb *aiocb);
int aio_error(const struct aiocb *aiocb);
ssize_t aio_return(const struct aiocb *aiocb);
int aio_suspend(const struct aiocb *const list[], int nent,
                const struct timespec *timeout);
int aio_cancel(int fd, struct aiocb *aiocb);

分散/聚集读写

struct iovec {
    void *iov_base;                     // starting address of buffer
    size_t iov_len;                     // size of buffer
};
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

固定长度读写

调用read/write函数返回的长度可能比需要的少,如果想要得到指定长度的读写量可以使用如下一组函数,它们不过是多次调用读写来实现。

ssize_t readn(int fd, void *buf, size_t nbytes);
ssize_t writen(int fd, void *buf, size_t nbytes);

内存映射IO

利用内存映射,可以直接向文件读写数据而不用调用read/write函数。

void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t off);
int mprotect(void *addr, size_t len, int prot); // change protection
int msync(void *addr, size_t len, int flags);   // flush change to mapping
int munmap(void *addr, size_t len);
addr
一般指定为0,由系统帮我们确定起始地址
len
一般指定为文件大小,通过fstat获取
prot
指定保护方式:PROT_READPROT_WRITEPROT_EXECPROT_NONE,要注意这里所指定的返回不能比调用open时传递的范围大。
flag
可以为:MAP_FIXEDMAP_SHAREDMAP_PRIVATE
fd
要操作的文件描述符
off
一般指定为0