基础IO 四

67 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第21天,点击查看活动详情

💦 标志位

为什么传两个标志位时需要使用 ‘ | ’ 操作符 ❓

O_WRONLY、 O_RDONLY、O_CREATE、O_APPEND 都是标志位。如果我们自己在设计 open 接口时,这里通常是使用整数,0 表示不要,1 表示要。而系统是怎么做的呢?—— 一个整数有 32 个比特位,所以一个标志位传一个整数是有点浪费的,所以这里可以用最低比特位表示是否读、第二低比特位表示是否写、第三低比特位表示是否追加等等,之后这里我们可以定义一些宏来,将来传入了 flags,系统要检测是什么标志位,它只需要 falgs & O_RDONLY,这也解释了为什么上面需要两个标志位时是 O_WRONLY|O_APPEND。

在这里插入图片描述

grep -ER 'O_CREAT|O_RDONLY' /usr/include/筛选标志位。

在这里插入图片描述

接着我们 vim 标志位所在路径,发现默认是只读,而 O_CREAT 以下是使用了八进制,不管如何,它们经过转换后,最终只有一个唯一比特位。我们也可以通过组合标志位,传入多个选项。

在这里插入图片描述

这里语言都要对系统接口做封装,本质是对兼容自身语法特性,系统调用使用成本较高,而且不具备可移植性,如果所有语言都用 open 这一套接口, 那么这套接口在 windows 下是不能运行的,所以你写的程序是不具备可移植性的,而 fopen 能在 Windows 和 Linux 下运行的原因是 C 语言对 open 进行了封装,也就是说这些接口会自动根据平台,选择底层对应的文件接口, 同样的 fopen,它在 Windows 和 Linux 中头文件的实现是不同的 。

💦 open的返回值

#include<stdio.h>  
#include<sys/types.h>  
#include<sys/stat.h>  
#include<fcntl.h>  
  
int main()  
{  
    int fd1 = open("log1.txt", O_WRONLY|O_APPEND|O_CREAT, 0644);
    int fd2 = open("log2.txt", O_WRONLY|O_APPEND|O_CREAT, 0644);
    int fd3 = open("log3.txt", O_WRONLY|O_APPEND|O_CREAT, 0644);
    int fd4 = open("log4.txt", O_WRONLY|O_APPEND|O_CREAT, 0644);
    int fd5 = open("log5.txt", O_WRONLY|O_APPEND|O_CREAT, 0644);

    printf("fd1: %d\n", fd1);                                   
    printf("fd2: %d\n", fd2);  
    printf("fd3: %d\n", fd3);  
    printf("fd4: %d\n", fd4);  
    printf("fd5: %d\n", fd5);  
    
    return 0;  
}                                    

在这里插入图片描述

我们说过返回小于 0 的数,则代表 open 失败,显示这里 open 都成功了。但是这里为什么不从 0 开始依次返回?—— 上面我们说过 C 程序运行起来,默认会打开三个文件(stdin、stdout、stderr),所以 0, 1, 2 分别与之对应。

为什么这里每打开一个文件所返回的文件描述符是类似数组下标的呢?—— 这里返回的文件描述符就是数组下标。一个进程是可以打开多个文件的,且系统内被打开的文件,一定是有多个的,那么这些多个被打开的文件,操作系统使用 “ 先描述,后组织 ” 的方式管理起来,描述一个打开文件的数据结构叫做 struct file,组织一堆 struct file 就是在 task_struct 中有一个 struct files_struct* files 指针,指向 struct files_struct,它的作用就是构建进程和文件之间的对应关系,其中包含了一个指针数组,这里我们可以理解为定长数组,struct file* fd_array[NR_OPEN_DEFAULT] ➡ #define NR_OPEN_DEFAULT BITS_PER_LONG ➡ #define BITS_PER_LONG 32。所以用户层看到的 fd 返回值,本质是系统中维护进程和文件对应关系的数组的下标。比如创建一个文件,会多一个 struct file,再把地址存储于指针数组中最小的且没有使用过的数组中,这里对应是 6 下标,然后把 6 作为返回值,返回给用户,所以当用户后续要对文件进行操作时就可以使用 fd 返回值作为参数,比如 read(fd) ,当前进程就会拿着 fd 去 struct files_struct* 指向的指针数组中找 fd 下标,根据 fd 下标对应的地址找到对应的文件,再在文件中找到对应的 read 方法,对 disk 中的数据进行读取。

在这里插入图片描述

Linux 2.6 内核源码验证:

在这里插入图片描述

对于 file_operations,不同硬件是有不同的方法的,大部分情况方法是和你的硬件驱动匹配的,虽然如此,但是最终文件通过函数指针实现你要打开的是磁盘,那就让所有的方法指向磁盘的方法,你要打开的是其它硬件,那就让所有的方法指向其它硬件的方法,而这里底层的差异,在上层看来,已经被完全屏蔽了。所以对进程来讲,对所有的文件进行操作,统一使用一套接口(现在我们明确了它是一组函数指针),换言之,对进程来说,你的操作和属性接口统一使用 struct file 来描述,所以在进程看来,就是 “ 一切皆文件 ”。

#include<stdio.h>  
#include<sys/types.h>  
#include<sys/stat.h>  
#include<fcntl.h>  
#include<unistd.h>  
  
int main()
{
    //close(0);
    //close(1);                                       
                                                      
    int fd1 = open("log.txt", O_CREAT|O_WRONLY, 0644);
    int fd2 = open("log.txt", O_CREAT|O_WRONLY, 0644);
                                      
    printf("hello bit!: %d\n", fd1);  
    printf("hello bit!: %d\n", fd2);  

	//fflush(stdout);
                        
    close(fd1);         
    close(fd2);         
                        
    return 0;           
}                       
  • 毫无疑问,这里 open 一个文件后的返回值是从 3 开始,但是这里进程一运行,close 0,此时再 open 一个文件后的返回值是 0,再 open 一个文件后的返回值是 3,从这里我们就可以知道,系统中,分配文件描述符的规则是按最小的,且没有被使用的下标进行分配。

  • 当我们 close 1,1 就不再指向显示器文件,此时 fd1 应该是 1,fd2 应该是 3,确实如此,然后再 printf 时,本来应该往显示器里写入,现在却往普通文件里写入,这种技术叫做输出重定向(echo "hello bit!" > temp.txt)。这里它输出重定向到了新打开的文件?—— 并没有,这里要先搞明白 fopen 和 open 之间的耦合关联。

    这里可以看下 FILE 结构体是被 _IO_FILE typedef 的(typedef struct_IO_FILE FILE),_IO_FILE 在 /usr/include/libio.h 下,在 _IO_FILE 结构中包含两个重要的成员:

      其一,底层对应的文件描述符下标 int _fileno,它是封装的文件描述符。换言之,在 C 的文件接口中,一定是使用 fileno 来调用系统接口 read(fp->fileno),所以 fopen 和 open 是通过 C 语言结构体内的文件描述符耦合的。

      其二,应用层 C 语言提供的缓冲区。记得曾经写进度条时,没有 \n,数据在缓冲区中不显示,必须以 fflush 强制刷新,其中数据所处的缓冲区就是由 __IO_FILE 维护的。

    在这里插入图片描述

    这里 close 1 后,1 下标就不再指向显示器文件,而是指向 log1.txt,FILE* stdout 当然还在,stdout 依然认为它的文件描述符值是 1,这里 printf 时会先把数据放到 C 语言提供的 __IO_FILE 缓冲区中,还没来得及刷新,已经把 fd1 关闭了,所以操作系统是没有办法由用户语言层刷新到操作系统底层的,所以自然也没看到结果。咦!这里不是有 \n 吗,为什么没有往操作系统刷新,因为此时 1 指向的是磁盘文件,磁盘文件是全缓冲,必须等待缓冲区满了再刷新,或者 fflush 强制刷新。显示器文件,无论是用户层还是内核层,都是行刷新,因为它无论怎样,最终都会往显示器上刷新的。

在这里插入图片描述