Linux系统编程之文件IO进阶

文章概括描述文段
上篇文章中介绍了linux中基本的io处理操作,对于一般的文件读取是没有问题的,但是考虑到终端以及网络等可能存在阻塞情况下,这时候就需要我们使用标志位 O_NONBLOCK 。其次,对于文件的阻塞和非阻塞操作,如果是一个已经打开的文件呢又该如何操作,fcntl 便应运而生了。

阻塞与非阻塞

读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。
现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情况:

  1. 正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。
  2. 就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时要兼顾用户体验,不能让和用户交互的进程响应太慢。

阻塞读终端

对于终端设备来说,如果终端没有输入,那么read将一直阻塞,知道用户输入enter后才会返回结果。

#include <unistd.h> 
#include <stdlib.h>
int main(void) {
    char buf[10];
    int n;
    // 阻塞读终端数据
    n = read(STDIN_FILENO, buf, 10); 
    if (n < 0) {
        perror("read STDIN_FILENO");
        exit(1); 
    }
    // 读成功,写到输出终端
    write(STDOUT_FILENO, buf, n);
    return 0; 
}

非阻塞读终端

如果在open一个设备时指定了O_NONBLOCK标志,read/write就不会阻塞。以read为例,如果设备暂时没有数据可读就返回-1,同时置errno为EWOULDBLOCK(或者EAGAIN,这两个 宏定义的值相同),表示本来应该阻塞在这里(would block,虚拟语气),事实上并有阻塞而是直接返回错误,调用者应该试着再读一次(again)。这种行为方式称为轮询 (Poll),调用者只是查询一下,而不是阻塞在这里死等,这样可以同时监视多个设备。

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#define DEV_TTY  "/dev/tty"
#define MSG_TRY  "try again \n"
int main(void)
{
    char buf[10];
    int fd,n;
    // 以非阻塞方式打开文件设备
    fd = open(DEV_TTY,O_RDONLY | O_NONBLOCK);
    if(fd<0)
    {
        perror("open device error!");
        exit(1);
    }
// 轮训标志
try_again:
    // 非阻塞读数据,立即返回
    n = read(fd,buf,sizeof(buf));
    if(n < 0){
        // 未读取到任何数据,重新读取
        if(errno == EAGAIN)
        {
            // 输出重读标志
            write(STDOUT_FILENO,MSG_TRY,strlen(MSG_TRY));
            // 睡眠一会
            sleep(3);
            goto try_again;
        }
        else
        {
            perror("read device error!");
            exit(1);
        }
    }
    // 写入输出终端
    write(STDOUT_FILENO,buf,n);
    close(fd);
    return 0;
}

用非阻塞I/O实现等待超时的例子。既保证了超时退出的逻辑又保证了有数据到
达时处理延迟较小。

非阻塞升级,等待超时

对于非阻塞读取终端数据,逻辑上是采用轮训的方式,这样处理起来有一点不好就是要一直的sleep睡眠等待。在一定程度上造成性能的浪费,。当然排除select,poll以及epoll等高效的异步IO转接方式,另一种可以替代的方式就是规定在一定的时间范围呢进行轮训,如果超时了就直接推出,给出有效提示。

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>

#define DEV_TTY  "/dev/tty"
#define MSG_TRY  "try again \n"
#define MSG_TIMEOUT "read device time out!\n"

int main(void)
{

    char buf[10];
    int fd,n;

    fd = open(DEV_TTY,O_RDONLY | O_NONBLOCK);

    if(fd<0)
    {
        perror("open device error!");
        exit(1);
    }

    // ls * 10 的超时等待
    for (int i = 0; i < 10; ++i)
    {
        /* code */
        n = read(fd,buf,sizeof(buf));
        if(n < 0){
            if(errno == EAGAIN)
            {
                // 未读取有效数据,睡眠等待轮训
                write(STDOUT_FILENO,MSG_TRY,strlen(MSG_TRY));
                sleep(1);
            }
            else
            {
                /* code */
                perror("read device error!");
                exit(1);
            }
        }
        else
        {
            // 读取到有效数据,直接打印输出
            write(STDOUT_FILENO,buf,n);
            close(fd);
            return 0;
        }
    }
    // 读取超时,打印输出超时信息
    write(STDOUT_FILENO,MSG_TIMEOUT,strlen(MSG_TIMEOUT));
    close(fd);
    return 0;
}

使用 fcntl 修改文件状态位

先前我们以read终端设备为例介绍了非阻塞I/O,为什么我们不直接对STDIN_FILENO做非阻塞read,而要重新open一遍/dev/tty呢?因为STDIN_FILENO在程序启动时已经被自动打开了,而我们需要在调用open时指定O_NONBLOCK标志。这里介绍另外一种办法,可以用 fcntl 函数改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志(这些标志称为File Status Flag),而不必重新open文件。

概述

#include <fcntl.h>

int fcntl(int fildes, int cmd, ...);

参数描述

filsed: 唯一文件描述符
cmd: 操作命令

根据cmd命令,我们可以选择对一打开文件描述符进行属性操作,例如通过命令 F_GETFD 来获取当前文件描述符的属性,通过 F_SETFD 来设置已经打开的文件描述符的属性,从而避免从新打开时添加对应的属性。

cmd 描述
F_DUPFD 复制文件描述词
F_GETFD 读取文件描述词标志
F_SETFD 设置文件描述词标志
F_GETFL 读取文件状态标志
F_SETFL 设置文件状态标志

更多设置参考 Man Page

在 F_SETFL 命令中,其中O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_NOCTTY 和 O_TRUNC不受影响,可以更改的标志有 O_APPEND,O_ASYNC, O_DIRECT, O_NOATIME 和 O_NONBLOCK。

返回值

返回值根据命令的不同有不同的返回值,例如 F_DUPFD 会返回一个新的文件描述符,F_GETFL 会返回文件的属性位等,如果出错会反悔 -1, 并且置位 errno,具体参考 Man Page。

实力代码

本片主要讲述文件属性值的设置,下面代码展示了如何通过 fctnl 函数对已经打开的文件进行非阻塞读取操作。

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>

#define DEV_TTY  "/dev/tty"
#define MSG_TRY  "try again \n"
#define MSG_TIMEOUT "read device time out!\n"

int main(void)
{

    char buf[10];
    int fd,n;

    fd = open(DEV_TTY,O_RDONLY);
    // 通过fcntl的命令 F_GETFL 获取打开文件的访问属性
    int flags = fcntl(fd,F_GETFL);
    printf("flags0 is %d\n",flags);
    // 属性添加 O_NONBLOCK
    flags |= O_NONBLOCK;
    printf("flags1 is %d\n",flags);
    // 通过命令 F_SETFL 将新的文件属性设置到文件中去
    fcntl(fd,F_SETFL, flags);

    if(fd<0)
    {
        perror("open device error!");
        exit(1);
    }

    for (int i = 0; i < 5; ++i){
        /* code */
        n = read(fd,buf,sizeof(buf));
        if(n < 0){
            if(errno == EAGAIN){
                write(STDOUT_FILENO,MSG_TRY,strlen(MSG_TRY));
                sleep(1);
            } else {
                /* code */
                perror("read device error!");
                exit(1);
            }
        } else {
            write(STDOUT_FILENO,buf,n);
            close(fd);
            return 0;
        }
    }

    write(STDOUT_FILENO,MSG_TIMEOUT,strlen(MSG_TIMEOUT));
    close(fd);
    return 0;
}

当然 fctnl 的功能还有很多,比如文件锁(读共享,写私有)等,后续篇幅会在锁机制中统一介绍,这里单纯的介绍一下设置文件非阻塞访问属性。

lseek修改文件访问指针

lseek函数的作用是用来重新定位文件读写的位移。

概述

#include <unistd.h>

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

参数解释

fildes: 文件描述符
offset: 为正则向文件末尾移动(向前移),为负数则向文件头部(向后移)
whence: 文件便偏移参考值

SEEK_SET:
  从文件头部开始偏移offset个字节
SEEK_CUR:
  从文件当前读写的指针位置开始,增加offset个字节的偏移量
SEEK_END:
  文件偏移量设置为文件的大小加上偏移量字节

返回值

如果设备不支持lseek,则lseek返回-1,并将errno设置为ESPIPE。
注意 fseek 和 lseek 在返回值上有细微的差别, fseek 成功时返回 0 ,失败时返回 -1 ,要返回当前偏移量需调用 ftell ,而 lseek 成功时返回当前偏移量失败时返回 -1 。

代码示例

// 获取文件大小
int lseek(fd,0,SEEK_END)

// 扩充文件大小
int add_len = 1024*8;
int fd=open("test.txt",O_RDWR);
if(fd == -1)
{
    perror("open test.txt");
    exit(-1);
}
lseek(fd,add_len-1,SEEK_END);

总结

经过前面博文中对 open,read,write,close 等函数的介绍,基本能够完成一般文件的 IO 操作,此篇幅有详细介绍了阻塞与非阻塞读取数据,文件属性修改函数 fcntl 的功能,其能够实现对打开文件的属性操作,函数功能非常强大。同时,lseek函数能够获取一打开文件的大小,还可以扩充文件的大小(lseek后必须进行一个write操作)。博文参考刑文鹏老师的Linux系统编程资料,特此记录以备不时之需。

参考学习资料邢文鹏老师

坚持原创技术分享,您的支持将鼓励我继续创作!