zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    详解MathType中如何插入特殊符号
    详解如何将MathType嵌入word中
    MathType公式编辑器快捷键操作
    MathType初级教程:怎么安装MathType
    AOPR密码过滤器
    教您如何在Word的mathtype加载项中修改章节号
    在word文档中如何插入Mathtype公式
    详解MathType中如何更改公式颜色
    静态缓存和动态缓存
    ThinkPHP U函数生成URL伪静态
  • 原文地址:https://www.cnblogs.com/elmluo/p/14761271.html
Copyright © 2011-2022 走看看