Linux文件I/O

138 阅读7分钟

文件I/O

内核为每个进程维护了一个打开文件的列表,该表称为文件表。文件表由非负整数组成,其中每项都代表一个打开的文件信息。

子进程会得到一份和父进程一样的文件表,其中打开的文件列表、访问模式,当前文件的位置等信息都是一样的,子进程文件表的变化不会影响到其他进程的文件表。

文件描述符使用C语言的int表示,文件描述符从0开始,默认上限1024。负数是不合法的文件描述符,通常使用-1来表示函数不能返回合法的文件描述符的错误。

每个进程通常会有3个默认的文件描述符:0,1,2。除非进程显示的关闭了他们。文件描述符0表示标准输入,文件描述符1表示标准输出,文件描述符2表示标准错误。C标准库提供了预处理器宏:STDIN_FILENO,STDOUT_FILENO和STDERR_FILENO宏,取代上边的整数直接引用。

遵循一切皆文件的理念,任何能读写的东西都可以使用文件描述符来访问。


打开文件

通过open函数打开文件获取文件描述符

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode)
int creat(const char *pathname, mode_t mode);

open()系统调用会将file指定的文件与成功返回的文件描述符相关联,文件位置指针会被设置为0,文件根据oflag给出的标志位打开。

oflag参数必须是以下参数之一:O_RDONLY, O_WRONLY 或 O_RDWR。指明文件是以只读,只写,可读写方式打开。

在使用O_CREATE创建新文件时,mode参数提供新文件的权限。

具体参数使用可以参考man手册。

open()和creat()调用成功都返回一个合法的文件描述符,失败返回-1,并将errno设置为一个合适的值。


read()读取文件

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

该系统调用从fd所指向的文件的当前偏移量之多读取count个字节到buf中,成功时返回写入buf缓冲区的字节数,失败返回-1,并设置errno。该文件位置指针会向前移动,移动长度由所读取的字节数确定。

读入所有字节

ssize_t ret;
while(count!=0 && (ret = read(fd, buf, count))!=0){
    if(ret == -1){
        if(errno == EINTR){
            continue;
        }
    perror("read");
    break;
    }
    count -= ret;
    buf += ret;
}

非阻塞读

在read()调用中如果文件中没有数据可读,read调用会阻塞,直到文件中有数据可读。有时候程序员不希望在read没有数据可读时调用阻塞,而是让调用立即返回,这种情况称为非阻塞I/O。

如果给出的文件描述符在非阻塞模式下打开(open()中给定O_NONBLOCK)并且没有可读数据,read()调用会返回-1,并且设置errno为EAGAIN,而不是阻塞。在进行非阻塞I/O时,必须检查EAGAIN,否则会出现数据缺失。


用write()写

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

一个write()调用由文件描述符引用文件的当前位置开始,将buf中至多count个字节写入文件。

成功时,返回写入的字节数。失败时,返回-1,并将errno设为对应的值。


同步I/O

最简单的确认数据写入磁盘的方法是使用fsync()调用.

#include <unistd.h>

int fsync(int fd);
int fdatasync(int fd);

调用fsync()可以保证fd对应文件的脏数据回写到磁盘上。文件描述符必须以写的形式打开。fdatasync()完成的事情和fsync()完成的事情一样,区别在于他仅写入数据,该调用不保证元数据同步在磁盘上,故此可能快一点。

成功时返回0,失败时返回1-,并设置errno。


关闭文件

#include <unistd.h>

int close(int fd);

close()调用解除了文件描述符和文件的关联,并分离进程和文件的关联。

成功时返回0,失败时返回1-,并设置errno。


lseek()查找

#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

lseek()系统调用可以为给定的文件描述符引用的文件位置设定指定值。

whence参数有以下三种:

  • SEEK_SET 将当前文件位置设置为offset;

  • SEEK_CUR 将当前文件位置设置为当前位置加上offset,offset可以为负数;

  • SEEK_END 将当前文件位置设置到文件结尾加上offset,offset可以为负数;

在调用成功时返回新文件位置,失败时返回-1,并设置errno;


定位读写

Linux定义了两种read()和write()的变体来替代lseek();

读形式的调用pread()和写形式的调用pwrite();

#include <unistd.h>

ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

pread()调用:读取fd文件描述符所指文件中offest偏移的count字节数据写入到buf中。

pwrite()调用:给fd文件描述符所指的文件中offert偏移位置写入count字节的buf中的数据。


截短文件

Linux提供两个系统调用来截短文件

#include <unistd.h>

int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);

两个系统调用都将指定的文件截短到length长度。

二者都在成功时返回0,失败时返回-1,并设置errno;


I/O多路复用

I/O多路复用允许应用在多个文件描述符上同时阻塞,并在其中某个可以读写时收到通知。

Linux提供了三种多路复用:select(),poll(),epoll();

select()系统调用

select()系统调用提供了一种实现同步I/O多路复用的机制。

#include <sys/select.h>

int select(int nfds, fd_set *restrict readfds,
           fd_set *restrict writefds, fd_set *restrict exceptfds,
           struct timeval *restrict timeout);

void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

在指定文件描述符准备好I/O之前,或者超过一定时间限制,select()系统调用就会阻塞。

监测的文件描述符可以分为三类,分别等待不同的事件。监测readfds中的文件描述符,确认其中是否有可读数据。监测writefds中文件描述符,确认其中是否有数据可写。监测exceptfds中的文件描述符,确认其中是否发生异常。

成功时每个集合只包含对应类型的I/O就绪的文件描述符。

第一个参数nfds等于所有文件描述符中最大值加一。

timeout指向一个timeval结构体。

FD_ZERO 从指定集合中移除所有文件描述符。

FD_SET 向指定集合中添加文件描述符。

FD_CLR 从指定集合中删除给定文件描述符。

FD_ISSET 测试一个文件描述符在不在指定集合中。

select()成功时返回三个集合中I/O就绪个数,失败时返回-1,应设置errno;

select示例程序

#include <bits/types/struct_timeval.h>
#include <stdio.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#define TIMEOUT 5
#define BUF_LEN 1024

int main(void){
    struct timeval tv;
    fd_set readfds;
    int ret;
    FD_ZERO(&readfds);
    FD_SET(STDIN_FILENO, &readfds);
    tv.tv_sec = TIMEOUT;
    tv.tv_usec = 0;
    ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv);
    if(ret == -1) {
        perror("select");
        return 1;
    }else if(!ret){
        printf("timeout %d",TIMEOUT);
    }
    if(FD_ISSET(STDIN_FILENO, &readfds)){
        char buf[BUF_LEN+1];
        int len;
        len = read(STDIN_FILENO, buf, BUF_LEN);
        if(len == -1){
            perror("read");
            return 1;
        }
        if(len){
            buf[len] = '\0';
            printf("read:%s\n",buf);
        }
        return 0;
    }
    return 0;
}

poll()系统调用

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

poll使用nfds个pollfd结构体构成的数组,fds指向该数组。

结构体定义如下:

#include <sys/poll.h>
    struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

每个pollfd结构体指定单一的文件描述符。可以传递多个结构体,让poll监视多个文件描述符。

poll示例程序

#include <stdio.h>
#include <unistd.h>
#include <sys/poll.h>
#define TIMEOUT 5 /* poll timeout, in seconds */
int main (void){
    struct pollfd fds[2];
    int ret;
    fds[0].fd = STDIN_FILENO;
    fds[0].events = POLLIN;
    fds[1].fd = STDOUT_FILENO;
    fds[1].events = POLLOUT;
    ret = poll (fds, 2, TIMEOUT * 1000);
    if (ret == -1) {
        perror ("poll");
        return 1;
    }

    if (!ret) {
        printf ("%d seconds elapsed.\n", TIMEOUT);
        return 0;
    }

    if (fds[0].revents & POLLIN)
        printf ("stdin is readable\n");
    if (fds[1].revents & POLLOUT)
        printf ("stdout is writable\n");
    return 0;
}