• Linux系统编程-文件IO


    目录

    1. 无处不在的系统调用

    • 但凡涉及与资源有关的操作、会影响其他进程的操作,都需要操作系统的介入支持,都需要通过系统调用来实现,其实系统调用从概念上来讲也不难理解。
    • 由操作系统实现并提供给外部应用程序的编程接口(Application Programming Interface,API),是应用程序同系统之间数据交互的桥梁。

    1.1 系统调用和库函数的区别?

    • 系统调用是操作系统向上层提供的接口。
    • 库函数是对系统调用的进一步封装。
    • 应用程序大多是通过高级语言提供的库函数,间接的进行系统调用。

    1.2 调用的简单过程

    标库函数和系统函数调用过程。

    2. C标准库的文件IO函数

    • fopen、fclose、fseek、fgets、fputs、fread、fwrite......
    • 在命令行,通过 man fopen...... 等可以查看系统定义的对应的标库函数。

    2.1 fopen 打开文件

    • r 只读、r+读写、w只写并截断为0、w+读写并截断为0、a追加只写、a+追加读写。
    • 这些字符串参数 mode 值后面也可以添加b,可以通过 man-pages 看到。
    函数 fopen 打开文件名为 path 指向的字符串的文件,将一个流与它关联。
    
           参数 mode 指向一个字符串,以下列序列之一开始 (序列之后可以有附加的字符):
    
           r      打开文本文件,用于读。流被定位于文件的开始。
    
           r+     打开文本文件,用于读写。流被定位于文件的开始。
    
           w      将文件长度截断为零,或者创建文本文件,用于写。流被定位于文件的开始。
    
           w+     打开文件,用于读写。如果文件不存在就创建它,否则将截断它。流被定位于文件的开始。
    
           a      打开文件,用于追加 (在文件尾写)。如果文件不存在就创建它。流被定位于文件的末尾。
    
           a+     打开文件,用于追加
                  (在文件尾写)。如果文件不存在就创建它。读文件的初始位置是文件的开始,但是输出总是被追加到文件
    的末尾。
    
           字符串                       mode                      也可以包含字母                      ``b''
           作为最后一个字符,或者插入到上面提到的任何双字符的字符串的两个字符中间。这样只是为了和      ANSI
           X3.159-1989  (``ANSI  C'')  标准严格保持兼容,没有实际的效果;在所有的遵循 POSIX 的系统中,``b''
           都被忽略,包括        Linux。(其他系统可能将文本文件和二进制文件区别对待,如果在进行二进制文件的
           I/O,那么添加 ``b'' 是个好主意,因为你的程序可能会被移植到非 Unix 环境中。)
    

    2.2 按字符读写 fgetc、fputc

    编译运行看对应的输出文件和控制台打印内容。

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    // 按照字符方式 fgetc(), fputc();
    void test01()
    {
      // 写文件
      // 可读可写的方式打开文件,没有就创建
      FILE *f_write = fopen("./test01.txt", "w+");
      if (f_write == NULL)
      {
        return;
      }
      char buf[] = "Read and write as characters";
      for (int i = 0; i < strlen(buf); i++)
      {
        fputc(buf[i], f_write);
      }
    
      // 关闭,会刷新缓冲区
      fclose(f_write);
    
      // 读文件
      FILE *f_read = fopen("./test01.txt", "r");
      if (f_read == NULL)
      {
        return;
      }
      char ch;
      while ((ch = fgetc(f_read)) != EOF)
      {
        printf("%c", ch);
      }
      fclose(f_read);
    }
    
    int main(int argc, char *argv[])
    {
      test01();
    }
    

    2.3 按行读写 fgets、fputs

    编译运行看对应的输出文件和控制台打印内容。

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    void test02()
    {
      // 写入文件
      // 可写的方式打开文件
      FILE *f_write = fopen("./test02.txt", "w");
      if (f_write == NULL)
      {
        return;
      }
      char *buf[] = {
          "hellow world
    ",
          "hellow world1
    ",
          "hellow world2
    "};
      int len = sizeof(buf) / sizeof(char *);
      for (int i = 0; i < len; i++)
      {
        fputs(buf[i], f_write);
      }
      fclose(f_write);
    
      // 读取文件
      FILE *f_read = fopen("./test02.txt", "r");
      char *s = NULL;
      while (!feof(f_read))
      {
        char buf[1024] = {0};
        fgets(buf, 1024, f_read);
        printf("%s", buf);
      }
      fclose(f_read);
    }
    
    int main(int argc, char *argv[])
    {
      test02();
    }
    
    

    2.4 按块读写文件 fread、fwrite

    • 主要针对于自定义的数据类型,可以通过 二进制 的方式读写。
    • 编译运行看对应的输出文件和控制台打印内容。
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    // 按照块读写文件(自定义的数据类型,二进制):fread() fwrite();
    void test03()
    {
      // 写文件
      FILE *f_write = fopen("./test03.txt", "wb");
      if (f_write == NULL)
      {
        return;
      }
      
      // 自定义结构体类型
      struct Person
      {
        char name[16];
        int age;
      };
    
      struct Person persons[5] =
          {
              {"zhangsan", 25},
              {"lisi", 25},
              {"wangwu", 25},
              {"zhuliu", 25},
              {"zhuoqi", 25},
          };
      int len = sizeof(persons) / sizeof(struct Person);
      for (int i = 0; i < 5; i++)
      {
        // 参数:数据地址、块的大小、块的个数、文件流
        fwrite(&persons, sizeof(struct Person), 5, f_write);
      }
      fclose(f_write);
    
      // 读文件
      FILE *f_read = fopen("./test03.txt", "rb");
      if (f_read == NULL)
      {
        return;
      }
      struct Person temp[5];
      fread(&temp, sizeof(struct Person), len, f_read);
      for (int i = 0; i < len; i++)
      {
        printf("name: %s, age: %d 
    ", temp[i].name, temp[i].age);
      }
    }
    
    int main(int argc, char *argv[])
    {
      test03();
    }
    

    2.5 按格式化读写文件 fprintf、fscanf

    编译运行看对应的输出文件和控制台打印内容。

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    void test04()
    {
      // 写文件
      FILE *f_write = fopen("./test04.txt", "w");
      if (f_write == NULL)
      {
        return;
      }
      fprintf(f_write, "hello world %d year - %d - month %d - day", 2008, 8, 8);
      fclose(f_write);
    
      // 读文件
      FILE *f_read = fopen("./test04.txt", "r");
      if (f_read == NULL)
      {
        return;
      }
      char buf[1024] = {0};
    
      while (!feof(f_read)) // 直到文件结束标识符,循环结束
      {
        fscanf(f_read, "%s", buf);
        printf("%s ", buf);
      }
      fclose(f_read);
    }
    
    
    int main(int argc, char *argv[])
    {
      test04();
    }
    

    3. 系统open、close函数

    3.1 通过man-pages查看函数

    • int open(const char *pathname, int flags);
    • int open(const char *pathname, int flags, mode_t mode);
    • int close(int fd);
    • 参数:文件路径、读写方式、权限设置(一般O_CREAT,权限用8进制,如0664)

    3.2 open 中 flags 参数说明

    • 头文件:fcntl.h 中定义
    • O_RDONLY :只读
    • O_WRONLY:只写
    • O_RDWR:读写
    • O_APPEND: 追加
    • O_CREAT: 文件存在就使用,不存在就创建
    • O_EXCL:文件不存就创建,存在则返回错误信息
    • O_TRUNC: 文件截断为0
    • O_NONBLOCK: 非阻塞的方式操作

    3.3 open 中 mode 参数并不是文件真正权限

    通过八进制创建文件的权限,在系统当中还要考虑umask。可以命令行运行 umask 进行查看。

    标库函数fopen的man-pages中也有关与这个 umask 的提及。

    计算公式:新建真实文件权限 = mode & ~umask

    如设置mode = 777,此时系统umask = 002,~umask取反得775,那么真实创建出来的文件权限 777 & 775 = 775;

      // 理解过程如下
      文件真实权限
      ⬇
      mode & ~umask
      ⬇
      777 & ~(002)
      ⬇
      777 & 775
      ⬇
      775
    

    3.4 open常见错误

    • 打开文件不存在
    • 以写方式打开只读文件(打开文件没有对应权限)
    • 以只写方式打开目录

    3.5 系统open函数打开文件

    编译运行输出

    #include <unistd.h> // open close 引入的头文件
    #include <fcntl.h>
    #include <stdio.h>
    #include <errno.h> // errno 需要的头文件
    
    int main(int argc, char *argv[])
    {
    
        // int fd = open("./dict.back", O_RDONLY | O_CREAT | O_TRUNC, 0777);
    
        int fd = open("./demo.txt", O_RDWR | O_TRUNC);
    
        printf("fd=%d 
    ", fd);
        
        // 这里关闭,下面代码中会产errno = 9;
        // close(fd);  
        
        if (fd != -1)
        {
            printf("open success");
        }
        else
        {
            // 出错的时候会产生一个errno, 对应不同的错误细节。
            printf("errno=%d 
    ", errno);
            printf("open failure");
        }
        
        close(fd);
        
        return 0;
    }
    

    4. PCB、文件描述符表、文件结构体

    4.1 文件描述符表、文件结构体、PCB结构体之间的关系图如下

    4.2 task_struct 结构体

    • 控制台中可使用命令 locate /include/linux/sched.h,如果没有locate 插件,可以根据系统提示命令行安装。
    • 如定位文件目录为:/usr/src/kernels/3.10.0-1160.11.1.el7.x86_64/include/linux/sched.h。
    • 打开文件可以看到,task_struct 中 保存了指向文件描述符表files指针。

    4.3 文件描述符表

    • sched.h 头文件中,PCB 结构体的成员变量 files_struct *file 指向文件描述符表。
    • 从应用程序使用角度,该指针可理解记忆成一个字符指针数组,通过下标 [0/1/2/3/4...] 找到对应的file结构体。
    • 本质是键值对, [0/1/2/3/4...] 分别对应具体file结构体地址。
    • 键值对使用的特性是自动映射的,系统会将自动找到使用下标的文件结构体。
    • 新打开文件,返回文件描述符表中未使用的最小文件描述符,这个系统自动进行管理。
    • 三个文件键是系统是默认打开,如果要用,使用系统定义的宏。
      • 0->宏STDIN_FILENO 指向标准输入文件。
      • 1->宏STDOUT_FILENO 指向标准输出文件。
      • 2->宏STDERR_FILENO 指向标准错误文件。
    • files_struct 结构体中成员变量,fd_array 为 file描述符数组。
    struct files_struct
    {
        // 引用累加计数
      atomic_t count; 
        ...
        // 文件描述符数组
      struct file * fd_array[NR_OPEN_DEFAULT]; 
    }
    

    4.4 FILE结构体

    • file结构体主要包含文件描述符、文件读写位置、IO缓冲区三部分内容。
    • open一个文件,内核就维护一个结构体,用来操作文件。
    • 结构体文件可以命令行定位 locate /include/linux/fs.h。
    • vim /usr/src/kernels/3.10.0-1160.11.1.el7.x86_64/include/linux/fs.h。

    举例说明常用的成员变量

    // 文件属性操作函数指针
    struct inode            *f_inode;       /* cached value */
    
    // 文件内容操作函数指针
    const struct file_operations    *f_op;
    
    // 打开的文件数量
    atomic_long_t           f_count;
    
    // O_RDONLY、O_NONBLOCK、O_SYNC(文件的打开标志)
    unsigned int            f_flags;
    
    // 文件的访问权限
    fmode_t                 f_mode;
    
    // 文件的偏移量
    loff_t                  f_pos;
    
    // 文件所有者
    struct fown_struct      f_owner;
    
    ...
    

    4.5 最大打开文件数

    单个进程默认打开文件的个数1024。命令查看unlimit -a 可看到open files 默认为1024。

    可以改通过提示的 (-n) 修改当前 shell 进程打开最大文件数量 ,命令行 ulimit -n 4096。
    但是只对当前运行进程生效,如果退出shell进程,再进入查看最大文件数变成原来的值1024

    通过修改系统配置文件永久修改该值(不建议)。
    vim /etc/security/limits.conf,按照格式要求修改。

    cat /proc/sys/fs/file-max 可以查看该电脑最大可以打开的文件个数,受内存大小影响。

    5. 系统read、write函数

    5.1 通过man-pages查看函数

    • ssize_t read(int fd, void *buf, size_t count);
    • ssize_t write(int fd, const void *buf, size_t count);
    • read与write函数类似,但注意 read、write 函数的第三个参数有所区别。
    int main(int argc, char *argv[]) {
        char buf[1024];
        int ret = 0;
        int fd = open("./dict.txt", O_RDWR);
        
        while(( ret = read(fd, buf, sizeof(buf)) ) != 0) {
            wirte(STDOUT_FILENO, buf, ret);
        }
        
        close(fd);
    }
    

    5.2 缓冲区的作用

    • 假设我们一次只读一个字节实现文件拷贝功能,使用read、write效率高,还是使用对应的标库函数fgetc、fputc效率高?
    • 根据调用的顺序,标库函数-系统调用-底层设备。调用一次系统函数,有个权级切换,比较耗时。
    • 所以标库函数理论上比系统调用的要快,通过下面两个小节来说明一下。

    5.2.1 标库函数fgetc、fputc使用的标库(用户)缓冲区

    • 过程:fgetc --> 库函数缓冲区 --> 系统调用write --> 磁盘
    • 标库函数有自己的缓冲区4096字节。
    • write(有用户区切换到 kernel 区这样的权级切换,一次刷4096字节)。
    • 示例代码如下通过fget、fputc 实现文件copy功能。
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(int argc, char *argv[]) {
        FILE *fp, *fp_out;
        int n;
        
        // 使用标库函数打开
        // r:只读,r+读写
        fp = fopen("./from.txt", "r");
        if (fp == NULL) {
            perror("fopen error");
            exit(1);
        }
        
        // w 只写,并且截断为0,w+ 读写,并且截断为0
        fp_out = fopen("./to.txt", "w");
        if (fp == NULL) {
            perror("fopen error");
        }
    
        // 先存到库函数去的缓存,4096字节,满了在调用系统函数写入磁盘。
        while ((n = fgetc(fp)) != EOF) {
            fputc(n, fp_out);
        }
    
        fclose(fp);
        fclose(fp_out);
    
        return 0;
    }
    

    5.2.2 系统调用read、write使用系统缓冲区

    • 过程: 系统调用write --> 磁盘
    • 内核也有一个缓冲区,默认大小4096字节。
    • 文件输入,先到缓冲区,充满再刷新到磁盘。
    • write(user区到kernel区权级切换,每次切换比较耗时。如果一次刷一个字节,切换的次数会特别的多,比较慢)。
    • read、write函数也可以称为 Unbuffered I/O,指的是无用户级缓冲区。但不保证不使用内核缓冲区。
    • 示例代码如下通过read、write 实现文件copy功能。
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <stdlib.h>
    #include <errno.h>
    
    // buf 缓存的大小。
    // #define N 1024
    #define N 1
    
    int main(int argc, char *argv[]) {
        int fd, fd_out;
        int n;
        char buf[N];
    
        fd = open("from.txt", O_RDONLY);
        if (fd < 0) {
            perror("open from.txt error");
            exit(1);
        }
    
        fd_out = open("to.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
        if (fd < 0) {
            perror("open to.txt error");
            exit(1);
        }
    
        while ((n = read(fd, buf, N))) {
            if (n < 0) {
                perror("read error");
                exit(1);
            }
            write(fd_out, buf, n);
        }
    
        close(fd);
        close(fd_out);
    
        return 0;
    }
    

    5.3 系统调用是否能被标库函数完全替代?

    • 既然标库函数减少了权级切换的次数,比系统调用快,但库函数也不能完全可以替代系统调用。
    • 比如需要保持实时性的场景,即时通讯的QQ、微信等软件。

    5.4 预输入缓输出

    • 用户区到内核区,权级切换比较耗时。所以通过缓存来提高读写效率。
    • 预输入: 文件Input,如果客户端需要100个字节,系统内核先从磁盘读满缓冲区4096字节(4KB),下一次读取的时候,从缓冲区里面读取。
    • 缓输出: 文件Output, 如果客户端需要输出100M字节内容到磁盘,先存满内核缓冲区4096字节(4KB),再由系统内核一次次的刷新到磁盘中。

    6. 系统错误处理函数

    6.1 exit 函数

    • 头文件:stdlib.h
    • 函数参数可以由开发人员约定,比如0表示正常退出,1表示异常退出。但是系统方法没有强制要求。
    ...
    if (fd < 0) {
      perror("open to.txt error");
      exit(1);  // 1表示异常,有开发人员相互协定
    }
    
    while ((n = read(fd, buf, N))) {
        if (n < 0) {
            perror("read error");
            exit(1);  // 1表示异常
        }
        write(fd_out, buf, n);
    }
    
    ...
    
    

    6.2 错误编号 errno

    • 对应不同类型错误编号和编号对应的描述。
    • 头文件:errno.h
    • 头文件位置: /usr/include/asm-generic/errno-base.h、/usr/include/asm-generic/errno.h

    ...
    // 如打开文件不存在, 查看errno对应的编号,代码如下
    fd = open("test", O_RDONLY);
    if (fd < 0)
    {
        printf("errno = %d
    ", errno);
        exit(1);
    }
    ...
    

    6.3 perror 函数

    • 会把上面errno对应的字符串描述一起拼接上,进行控制台打印。
    • void perror(const char *s)
    ...
    // 以写方式打开一个目录
    // fd = open("testdir", O_RDWR);
    fd = open("testdir", O_WRONLY);
    if (fd < 0)
    {
        perror("open testdir error");
        exit(1);
    }
    ...
    

    6.4 strerror 函数

    • 返回错误编号对应的描述
    • 头文件:string.h
    • char *strerror(int errnum);

    printf ("open testdir error", strerror(errno));

    6.5 错误处理的代码示例

    #include <unistd.h> //read write
    #include <fcntl.h>  //open close O_WRONLY O_RDONLY O_CREAT O_RDWR
    #include <stdlib.h> //exit
    #include <errno.h>
    #include <stdio.h> //perror
    #include <string.h>
    
    int main(void)
    {
        int fd;
    #if 0
        //打开文件不存在
        // fd = open("test", O_RDONLY | O_CREAT);
        fd = open("test", O_RDONLY);
        if (fd < 0)
        {
            printf("errno = %d
    ", errno);
            printf("open test error: %s
    ", strerror(errno));
            exit(1);
        }
        printf("open success");
    #elif 0
        // 打开的文件没有对应权限(以只写方式打开一个只有读权限的文件)
        if (fd < 0)
        {
            fd = open("test", O_WRONLY);
            // fd = open("test", O_RDWR);
            printf("errno = %d
    ", errno);
            perror("open test error");
            exit(1);
        }
        printf("open success");
    
    #endif
    #if 1
        // 以写方式打开一个目录
        // fd = open("testdir", O_RDWR);
        fd = open("testdir", O_WRONLY);
        if (fd < 0)
        {
            perror("open testdir error");
            exit(1);
        }
    #endif
    
        return 0;
    }
    

    7. 阻塞、非阻塞

    7.1 阻塞和非阻塞概念

    读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。

    现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情况:

    正在被调度执行: CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。

    就绪状态: 该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时要兼顾用户体验,不能让和用户交互的进程响应太慢。

    7.2 终端设备

    • 文件描述符:STDIN_FILENO、STDOUT_FILE、STDERR_FILENO;
    • 上面三个文件描述符对应都是一个设备文件,/dev/tty。
    • 从控制台输入内容到设备文件,这个过程就是阻塞的,对应STDIN_FILENO,会等待用户输入。
    • 阻塞与非阻塞是对于设备文件而言。

    7.3 阻塞读终端

    ...
       // 默认是用阻塞的方式
       fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);
    
        if (fd < 0)
        {
            perror("open /dev/tty");
            exit(1);
        }
        else
        {
            printf("fd: %d", fd);
        }
    ... 
    

    7.4 非阻塞读终端(O_NONBLOCK)

    #include <unistd.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    #define MSG_TRY "try again
    "
    
    int main(void)
    {
        char buf[10];
        int fd, n;
    
        // 默认是阻塞的方式
        // fd = open("/dev/tty", O_RDONLY);
    
        // 使用 O_NONBLOCK 标志,设置非阻塞读终端
        fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);
    
        if (fd < 0)
        {
            perror("open /dev/tty");
            exit(1);
        }
        else
        {
            printf("fd: %d", fd);
        }
    tryagain:
    
        //-1 出错  errno==EAGAIN 或者 EWOULDBLOCK
        n = read(fd, buf, 10);
    
        if (n < 0)
        {
            // 由于 open 时指定了 O_NONBLOCK 标志,
            // 通过 read 读设备,没有数据到达返回-1,同时将 errno 设置为 EAGAIN 或 EWOULDBLOCK
            
            if (errno != EAGAIN)
            {
                perror("read /dev/tty");
                exit(1);
            }
            sleep(3);
            write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
            goto tryagain;
        }
        write(STDOUT_FILENO, buf, n);
        close(fd);
    
        return 0;
    }
    
    

    7.5 非阻塞读终端和等待超时

    #include <unistd.h>
    #include <fcntl.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    #define MSG_TRY "try again
    "
    #define MSG_TIMEOUT "time out
    "
    
    int main(int argc, char *argv[])
    {
        char buf[10];
        int i;
        int fd;
        int n;
        
        //  使用 NONBLOCK 非阻塞
        fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); 
    
        if (fd < 0)
        {
            perror("open /dev/tty");
            exit(1);
        }
    
        printf("open /dev/tty success ... %d 
    ", fd);
    
        // timeout
        for (i = 0; i < 5; ++i)
        {
            n = read(fd, buf, 10);
            if (n > 0)
            {
                // 读到了东西,直接跳出循环
                break;
            }
            if (n != EAGAIN)
            {
                // EWOULDBLK
                perror("read /dev/tty");
                exit(1);
            }
            sleep(1);
            write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
        }
        if (i == 5)
        {
            write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
        }
        else
        {
            write(STDOUT_FILENO, buf, n);
        }
        close(fd);
        return 0;
    }
    

    7.6 read 函数返回值

    7.6.1 返回 >0

    实际读取到的字节数

    7.6.2 返回 0

    读到文件末尾

    7.6.3 返回 -1

    • errno != EAGAIN(或!= EWOULDBLOCK) read出错
      • EAGAIN: enable again,Resource temporarily unavailable 表示资源短暂不可用,这个操作可能等下重试后可用。
      • EWOULDBLOCK:用于非阻塞模式,不需要重新读或者写
    • errno == EAGAIN (或== EWOULDBLOCK) read 正常,只不过没有数据到达而已
      • 读取了设备文件,设置了非阻塞读,并且没有数据到达。

    8. lseek 函数

    8.1 文件偏移

    • Linux中可使用系统函数lseek来修改文件偏移量(读写位置)。
    • 每个打开的文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个字节就会将读写位置往后移多少个字节。
    • 但是有一个例外,如果以O_APPEND方式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。
    • lseek和标准I/O库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)。

    8.2 标库 fseek 函数

    • int fseek(FILE *stream, long offset, int whence)
    • fseek常用参数。 SEEK_SET、SEEK_CUR、SEEK_END
    • 成功返回0;失败返回-1
    • PS:超出文件末尾位置返回0;往回超出文件头位置,返回-1

    8.3 系统 lseek 函数

    • lseek (int fd, off_t offset, int whence)
    • lseek常用参数。 SEEK_SET、SEEK_CUR、SEEK_END
    • 失败返回 -1;成功返回 较文件起始位置向后的偏移量。
    • PS:lseek允许超过文件结尾设置偏移量,文件会因此被扩容。并且文件“读”和“写”使用同一偏移位置。
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <fcntl.h>
    
    int main(int argc, char *argv[])
    {
        int fd;
        int n;
        int ret;
    
        char msg[] = "It's a test for lseek 
    ";
        char ch;
    
        fd = open("lseek.txt", O_RDWR | O_CREAT, 0644);
    
        if (fd < 0)
        {
            perror("open lseek.txt error");
            exit(1);
        }
    
        // 使用fd对打开的文件进行写操作,写完光标指针位于文件内容结尾处。
        write(fd, msg, strlen(msg));
    
        // 将文件内容指针,重置,设置从0开始,偏移12个位置。返回偏移量。
        ret = lseek(fd, 12, SEEK_SET);
    
        printf("offset len: %d 
    ", ret);
    
        while (n = read(fd, &ch, 1))
        {
            if (n < 0)
            {
                perror("read error");
                exit(1);
            }
    
            // 将文字内容按照字节读出,写到屏幕
            write(STDOUT_FILENO, &ch, n);
        }
    
        close(fd);
        
        return 0;
    }
    

    8.4 lseek 常用操作

    • 文件的读写,使用一个光标指针,写完文件,再去读的话,需要重新设置指针目标。
    • PS: lseek函数返回的偏移量总是相对于文件头而言。

    8.4.1 使用lseek拓展文件

    • write操作才能实质性的拓展文件。
    • 单单lseek是不能进行拓展的,需要加一次实质性的IO操作。
    • 一般如write(fd, "c", 1); 加一次实质性的IO操作。
    • 查看文件的16进制表示形式 od -tcx 文件名。
    • 查看文件的10进制表示形式 od -tcd 文件名。

    8.4.2 标库 truncate 函数

    • 截断文件到具体specific长度,传入通过文件路径。
    • int truncate(const char *path, off_t length)。
    • 使用这个方法,文件必须可写。
    • 成功返回0;失败返回-1和设置errno。

    8.4.3 系统 ftruncate 函数

    • 截断文件到具体specific长度,传入文件描述符。
    • 使用这个方法,文件必须open,且拥有可写权限。
    • int ftruncate(int fd, off_t length)。
    • 成功返回0;失败返回-1和设置errno。

    8.4.4 通过lseek获取文件的大小

    int ret = lseek(fd, 0, SEEK_END);

    8.4.5 综合示例代码如下

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <string.h>
    
    int main(int argc, char *argv[])
    {
        int fd;
        int ret_len;
        int ret_truncate;
    
        fd = open("lseek.txt", O_RDWR | O_TRUNC | O_CREAT, 0664);
        if (fd < 0)
        {
            perror("open lseek.txt error");
            exit(1);
        }
    
        // 可以用来文件长度, 从末尾开始,偏移到头。返回偏移量
        ret_len = lseek(fd, 0, SEEK_END);
    
        if (ret_len == -1)
        {
            perror("lseek error");
            exit(1);
        }
    
        printf("len of msg = %d
    ", ret_len);
    
        // truncate(const char *path, off_t length) 截断文件到具体长度,文件必须可写, 成功返回0,失败返回-1
    
        // ftruncate(int fd, off_t length) 截断文件到具体长度,文件必须打开,成功返回0,失败返回-1
    
        ret_truncate = ftruncate(fd, 1800);
    
        if (ret_truncate == -1)
        {
            perror("ftruncate error");
            exit(1);
        }
    
        printf("ftruncate file success, and ret_truncate is %d 
    ", ret_truncate);
    #if 1
    
        ret_len = lseek(fd, 999, SEEK_SET);
        if (ret_len == -1)
        {
            perror("lseek seek_set error");
            exit(1);
        }
    
        int ret = write(fd, "a", 1);
        if (ret == -1)
        {
            perror("write error");
            exit(1);
        }
    
    #endif
    
    #if 0
        off_t cur = lseek(fd, -10, SEEK_SET);
        printf(" ****** %ld 
    ", cur);
        if (cur == -1) {
            perror("lseek error");
            exit(1);
        }
    #endif
        close(fd);
        return 0;
    }
    

    9. fcntl 函数

    • 头文件 fcntl.h
    • 文件控制 file control,改变一个已经打开的文件的访问控制属性。不需要重新open设置。
    • int fcntl(int fd, int cmd, ... /* arg */ )
    • 两个参数,F_GETFL 和 F_SETFL 重点需要掌握

    9.1 F_GETFL(get file flags)

    获取文件描述符,对应文件的属性信息

    9.2 F_SETFL(set file flags)

    设置文件描述符,对应文件的属性信息

    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <string.h>
    
    #define MSG_TRY "try again 
    "
    
    int main(int argc, char *argv[])
    {
        char buf[10];
        int flags;
        int n;
    
        // 获取stdin属性信息
        flags = fcntl(STDIN_FILENO, F_GETFL);
        if (flags == -1)
        {
            perror("fcntl error");
            exit(1);
        }
    
        // 位或操作,加入非阻塞操作权限(这样文件不用重新通过设置权限的方式打开)
        flags |= O_NONBLOCK;
        
        int ret = fcntl(STDIN_FILENO, F_SETFL, flags);
        if (ret == -1)
        {
            perror("fcntl error");
            exit(1);
        }
    
    tryagain:
        n = read(STDIN_FILENO, buf, 10);
        if (n < 0)
        {
            if (errno != EAGAIN)
            {
                perror("read /dev/tty");
                exit(1);
            }
            sleep(3);
            write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
            goto tryagain;
        }
        write(STDOUT_FILENO, buf, n);
        return 0;
    }
    

    10. ioctl函数

    • 头文件:sys/ioctl.h,文件位置 locate sys/ioctl.h。
    • 主要应用于设备驱动程序中,对设备的I/O通道进行管理,控制设备特性。
    • 通常用来获取文件的物理特性,不同文件类型所含有的特性值各不相同。
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/ioctl.h>
    
    int main(void) {
        // 定义一个包含窗口大小的结构体。
        struct winsize size;
        
        // isatty 如果是不是终端,返回0
        if (isatty(STDOUT_FILENO) == 0) {
            exit(1);
        }
        
        if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) < 0) {
            perror("ioctl TIOCGWINSZ error");
            exit(1);
        }
        
        // 输出控制台行和列
        printf("%d rows, %d colums 
    ", size.ws_row, size.ws_col);
        
        return 0;
    }
    

    在这儿,特感谢大家观看!如有不妥之处,还请大家批评指正,大家可以联系我,或在下方评论,谢谢大家!

    本文来自博客园,作者:骆三疯,转载请注明原文链接:https://www.cnblogs.com/elmluo/p/14761271.html

  • 相关阅读:
    leetcode 279. Perfect Squares
    leetcode 546. Remove Boxes
    leetcode 312. Burst Balloons
    leetcode 160. Intersection of Two Linked Lists
    leetcode 55. Jump Game
    剑指offer 滑动窗口的最大值
    剑指offer 剪绳子
    剑指offer 字符流中第一个不重复的字符
    leetcode 673. Number of Longest Increasing Subsequence
    leetcode 75. Sort Colors (荷兰三色旗问题)
  • 原文地址:https://www.cnblogs.com/elmluo/p/14761271.html
Copyright © 2020-2023  润新知