🍎作者:阿润菜菜
📖专栏:Linux系统编程
码字不易,请多多支持😘😘
这是目录
重新认识文件
- 是不是只有C/C++有文件操作呢?python、java、go等文件接口操作的方法是不太一样的,那如何理解这种现象?有没有统一的视角去看待所有的语言文件操作呢?—我们今天从系统视角去理解 ---- 实际都是通过系统调用来访问
- 文件=内容+属性 — 针对文件的操作:对内容的操作,对属性的操作,对内容和属性的操作
- 文件可以分为两大类:磁盘文件 和 被打开的文件(内存文件)
- 当文件没有被操作的时候,文件一般放在磁盘位置。空文件也在磁盘中占据空间,因为文件属性也是数据,保存数据就需要空间。
- 我们在文件操作的时候,文件需要在哪里?—内存,依据冯诺依曼体系的规定
- 所以我们在文件操作的时候,文件需要提前load到内存,那load是内容还是属性?至少有属性吧!那是不是只有你一个人在load呢?当然不是,内存中一定存在大量的不同文件的属性
- 所以,打开文件的本质就是将需要的文件加载到内存中,OS内部一定会同时存在大量的被打开的文件,那操作系统需不需要管理呢?怎么管理?— 先描述,在组织!
- 先描述 — 构建在内存中的文件结构体 struct file (文件从磁盘中来,struct file* next连接下一个文件信息)。在组织 — struct file结构体利用某种数据结构链接起来。在OS内部,对被打开的文件进行管理,就转换成了对类似链表的增删查改
- 结论:文件被打开,OS要为被打开的文件,创建对应的内核数据结构
- 所有文件操作的本质就是进程和被打开文件的关系。 — struct task_struct 和 struct file
系统内部的文件操作
库函数底层必须调用系统调用接口,因为无论什么进程想访问文件,都必须按照操作系统提供的方式来进行访问,所以就算文件操作相关函数千变万化,但是底层是不变的,这些函数最后都会调用系统调用接口,按照操作系统的意愿来合理的访问磁盘上的文件。
我们不能用语言绕过操作系统去操纵硬件,所以必须通过系统调用通过操作系统来进行文件操作!不管什么编程语言,只是不同语言对系统调用进行了各自不同的封装,所以对这些文件操作接口的理解,其实就要落实到对系统调用接口的理解! 也就是说所有的只要要访问硬件或者操作系统内部的资源,都要通过系统调用!避不开的!
我们C语言的文件操作
C语言文件操作接口主要包括以下几类:
- 打开和关闭文件的接口,如fopen(), fclose()等。这些接口用于创建或打开一个文件,并返回一个FILE类型的指针,以及关闭一个已打开的文件,并释放相关资源。
- 顺序读写数据的接口,如fgetc(), fputc(), fgets(), fputs(), fprintf(), fscanf()等。这些接口用于从文件中读取或写入字符、字符串或格式化数据,并自动移动文件指针。
- 随机读写数据的接口,如fread(), fwrite(), fseek(), ftell()等。这些接口用于从文件中读取或写入二进制数据块,并根据指定位置移动或获取文件指针。
- 其他辅助功能的接口,如feof(), ferror(), clearerr()等。这些接口用于检测文件是否到达末尾、是否发生错误、以及清除错误标志。
文件的打开方式:
r:以只读的方式打开文件,若文件不存在就会出错。
w:以只写的方式打开文件,文件若存在则清空文件内容重新开始写入,若不存在则创建一个文件。
a:以只写的方式打开文件,文件若存在则从文件尾部以追加的方式进行写入,若不存在则创建一个文件。
r+:以可读写的方式打开文件,若文件不存在就会出错。
w+:以可读写的方式打开文件,其他与w一样。
a+:以可读写的方式打开文件,其他与a一样。
当然这些也都是C库提供的函数,是对系统调用的上层封装,在系统级别文件操作我们是通过系统调用实现的:
系统内部的文件操作
文件操作系统调用接口是指Linux内核提供的一组用于对文件进行打开、读写、关闭等操作的函数。它们包括以下几个常用的函数:
- open:打开一个文件,返回一个文件描述符,可以指定文件的打开方式和权限。
- write:向一个已打开的文件中写入数据,返回实际写入的字节数。
- read:从一个已打开的文件中读取数据,返回实际读取的字节数。
- lseek:改变一个已打开文件的读写位置,返回新的偏移量。
- close:关闭一个已打开的文件,释放资源。
这些函数都需要传入一个文件描述符作为参数,它是一个非负整数,用于标识不同的打开文件。每个进程都有自己独立的一组文件描述符,并且默认有三个预定义的描述符:0代表标准输入,1代表标准输出,2代表标准错误输出。
这些函数都有可能失败,并返回-1,并设置errno变量为相应的错误码。因此,在调用这些函数后,需要检查返回值和错误码来判断是否成功。
我们主要介绍前三个:
OS一般会如何让用户给自己传递标志位的?多个标志位怎么实现呢? — 位图
其实是通过位操作实现的:
#include<stdio.h>#defineONE0x1#defineTWO0x2#defineTHREE0x4#defineFOUR0x8#defineFIVE0x10// 0000 0000 0000 0000 0000 0000 0000 0000voidPrint(int flags){if(flags & ONE)printf("hello 1\n");//充当不同的行为if(flags & TWO)printf("hello 2\n");if(flags & THREE)printf("hello 3\n");if(flags & FOUR)printf("hello 4\n");if(flags & FIVE)printf("hello 5\n");}intmain(){printf("--------------------------\n");Print(ONE);printf("--------------------------\n");Print(TWO);printf("--------------------------\n");Print(FOUR);printf("--------------------------\n");Print(ONE|TWO);printf("--------------------------\n");Print(ONE|TWO|THREE);printf("--------------------------\n");Print(ONE|TWO|THREE|FOUR|FIVE);printf("--------------------------\n");return0;}
open:打开一个文件,返回一个文件描述符,可以指定文件的打开方式和权限
open有两种调用方式:
一种是只传入文件名和访问模式,另一种是还传入创建权限(如果需要创建新文件)。访问模式有必需部分和可选部分,必需部分是 O_RDONLY(只读)、O_WRONLY(只写)或 O_RDWR(读写),可选部分有 O_APPEND(追加)、O_TRUNC(截断)、O_CREAT(创建)、O_EXCL(排他)等。创建权限是由几个标志按位或得到的,如 S_IRUSR(用户读)、S_IWUSR(用户写)、S_IXUSR(用户执行)等。
字符串/0 问题: 系统调用不需要这个!
使用 open 函数打开一个文件,如果不存在则创建一个新文件,并设置访问模式为读写和追加,创建权限为用户读写和组读写:
#include<stdio.h>#include<stdlib.h>#include<fcntl.h>#include<sys/stat.h>intmain(){// 打开或创建一个文件int fd =open("test.txt", O_RDWR | O_APPEND | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);if(fd ==-1){// 打开失败,打印错误信息perror("open error");exit(1);}// 打开成功,打印文件描述符printf("open success, fd = %d\n", fd);// 关闭文件close(fd);return0;}
创建目录的命令mkdir,目录起始权限默认是0777,创建文件的命令touch,文件起始权限是0666,这些命令的实现实际上是要调用系统接口open的,并且在创建文件或目录的时候要在open的第三个参数中设置文件的起始权限。
25intmain()26{27umask(0);//将进程的umask值设置为00002829// C语言中的w选项实际上底层需要调用这么多的选项O_WRONLY O_CREAT O_TRUNC 066630// C语言中的a选项需要将O_TRUNC替换为O_APPEND31int fd =open(FILE_NAME,O_WRONLY | O_CREAT,0666);//设置文件起始权限为066632if(fd <0)33{34perror("open");35return1;//退出码设置为136}37close(fd);38}
### write:向一个已打开的文件中写入数据,返回实际写入的字节数
**write:向一个已打开的文件中写入数据,返回实际写入的字节数。需要传入文件描述符、数据缓冲区和数据长度。如果返回值小于请求的字节数,可能是因为错误或者设备驱动程序对数据块长度敏感。如果返回值为 0,表示没有写入任何数据;如果返回值为 -1,则表示出现错误。**
使用 write 函数向一个已打开的文件中写入一段字符串,并检查返回值是否正确:
```c
#include<stdio.h>#include<stdlib.h>#include<unistd.h>intmain(){// 要写入的字符串和长度char*str ="Hello world!\n";int len =13;// 向标准输出(文件描述符为1)写入字符串int ret =write(1, str, len);if(ret ==-1){// 写入失败,打印错误信息perror("write error");exit(1);}if(ret != len){// 写入字节数不正确,打印警告信息fprintf(stderr,"write warning: expected %d bytes, but got %d bytes\n", len, ret);}// 写入成功,打印返回值printf("write success, ret = %d\n", ret);}
read:从一个已打开的文件中读取数据,返回实际读取的字节数
read:从一个已打开的文件中读取数据,返回实际读取的字节数。需要传入文件描述符、数据缓冲区和数据长度。如果返回值小于请求的字节数,可能是因为错误或者已到达文件尾。如果返回值为 0,表示没有读取任何数据;如果返回值为 -1,则表示出现错误。
使用 read 函数从一个已打开的文件中读取一定长度的数据,并存储到一个缓冲区中,并检查返回值是否正确:
#include<stdio.h>#include<stdlib.h>#include<unistd.h>intmain(){// 要读取的字节数和缓冲区大小 int len =100;char buf[100];// 从标准输入(文件描述符为0)读取数据到缓冲区中int ret =read(0, buf, len);if(ret ==-1){// 读取失败,打印错误信息perror("read error");exit(1);}if(ret ==0){// 读取到文件尾,没有数据可读,打印提示信息printf("read end of file\n");}// 读取成功,打印返回值和缓冲区内容(注意添加结束符)printf("read success, ret = %d\n", ret);
buf[ret]='\0';printf("buf: %s\n", buf);}
使用这些接口时,有一些事项需要注意:
- 在调用 open 函数时,要根据文件的用途和状态选择合适的访问模式和创建权限。如果使用了 O_CREAT 标志,要指定创建权限,否则可能导致文件权限不正确。如果使用了 O_EXCL 标志,要检查返回值是否为 -1,否则可能导致覆盖已有文件。如果打开的是设备文件或符号链接,要注意一些特殊的访问模式,如 O_NONBLOCK、O_NOCTTY、O_NOFOLLOW 等。
- 在调用 write 函数时,要保证数据缓冲区的有效性和长度正确性。如果写入的是文本文件,要注意添加换行符或结束符。如果写入的是二进制文件,要注意字节序和对齐问题。如果写入的是设备文件或网络套接字,要注意数据块长度和超时问题。
- 在调用 read 函数时,要保证数据缓冲区的有效性和大小足够。如果读取的是文本文件,要注意处理换行符或结束符。如果读取的是二进制文件,要注意字节序和对齐问题。如果读取的是设备文件或网络套接字,要注意数据块长度和超时问题。
- 在调用这些接口后,都要检查返回值是否为 -1,并根据 errno 变量来判断错误原因,并进行相应的处理或提示。有些错误可能是暂时性的或可恢复的,如 EINTR、EAGAIN、EWOULDBLOCK 等;有些错误可能是严重性的或不可恢复的,如 EACCES、EBADF、EFAULT、EINVAL 等。
看看Linux内核源代码是怎么说的
理解文件控制块&&文件描述符&&文件指针的关系
进程可以打开多个文件,对于大量的被打开文件,操作系统一定是要进行管理的,也就是先描述再组织,所以操作系统会为被打开的文件创建对应的内核数据结构,也就是文件控制块FCB,在linux源码中是struct file{}结构体,包含了文件的大部分属性
#include<assert.h>#include<stdio.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#defineFILE_NAME(number)"log.txt"#numberintmain(){int fd0 =open(FILE_NAME(1),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666int fd1 =open(FILE_NAME(2),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666int fd2 =open(FILE_NAME(3),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666int fd3 =open(FILE_NAME(4),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666int fd4 =open(FILE_NAME(5),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666printf("fd:%d\n",fd0);printf("fd:%d\n",fd1);printf("fd:%d\n",fd2);printf("fd:%d\n",fd3);printf("fd:%d\n",fd4);close(fd0);close(fd1);close(fd2);close(fd3);close(fd4);}
结果:
通过上述讲解,我们知道open系统调用会返回文件描述符,那它为什么是从3开始呢??
其实main函数会默认打开这三个标准文件:
这三个标准文件是:
- 标准输入(stdin):用于从键盘或其他输入设备读取数据,通常对应文件描述符0。可以使用C语言的scanf、getchar等函数或者Linux的read系统调用来读取标准输入。
- 标准输出(stdout):用于向屏幕或其他输出设备写入数据,通常对应文件描述符1。可以使用C语言的printf、putchar等函数或者Linux的write系统调用来写入标准输出。
- 标准错误输出(stderr):用于向屏幕或其他输出设备写入错误信息,通常对应文件描述符2。可以使用C语言的fprintf、perror等函数或者Linux的write系统调用来写入标准错误输出。
这三个标准文件在程序启动时就被自动打开,并且在程序结束时被自动关闭,无需手动操作。它们也可以被重定向到其他文件或设备,例如使用 > 或 < 符号。
所以为什么open文件操作后返回值 是3? 因为 0 1 2 已经被占用了 ---- 本质是数组下标
内存中文件描述符,文件描述符表,文件控制块,进程控制块的关系如下图所示,文件描述符表,说白了就是一个存储指向文件控制块的指针的指针数组,而文件描述符就是这个指针数组的索引,进程控制块中会有一个指向文件描述符表的指针。通过文件描述符就可以找到对应的被打开的文件。
操作系统通过这些内核数据结构,将被打开的文件和进程联系起来。
深度理解
文件描述符的实质:文件描述符是内核为每个进程维护的一个打开文件记录表的索引值
C语言如何访问系统? 就是通过文件描述符;同样的C++的cin、cout等类中也必须有文件描述符!没有文件描述符,怎么通过操作系统访问(系统调用)外设呢! 每个编程语言都是如此!
通过上述的引出,我们可以知道文件描述符的实质是:
- 文件描述符是一个非负整数,用于标识不同的已打开文件。
- 文件描述符是内核为了高效管理已打开文件所创建的索引,它可以用来调用各种I/O系统调用函数。
- 文件描述符是进程级别的,每个进程都有自己独立的一组文件描述符,并且默认有三个预定义的描述符:0代表标准输入,1代表标准输出,2代表标准错误输出。
- 文件描述符可以被复制、重定向、关闭等操作,但不能被直接读写。要读写一个已打开文件,需要使用read、write等系统调用函数,并传入相应的文件描述符作为参数。
文件描述符表和file结构体之间的关系是:
- 文件描述符表是内核用来存储每个进程的文件描述符和对应的打开文件信息的表格。每个进程在其进程控制块(PCB)中都保存着一份文件描述符表
- file结构体是内核用来表示已打开文件的数据结构,它包含了当前读写位置、访问模式、状态标志等信息,以及指向对应inode对象或者i-node表项的指针。file结构体也可以称为打开文件句柄或者打开文件表项。
- 文件描述符表和file结构体之间通过指针相互连接,一个文件描述符可以指向一个或多个file结构体,一个file结构体也可以被一个或多个文件描述符所指向。这样可以实现不同进程或同一进程中不同文件描述符共享同一个已打开文件。
文件描述符的分配规则:系统在创建文件描述符时会寻找当前未使用的最小下标
关闭012文件描述符产生的现象(新打开文件的fd被赋值为0或1或2)
当关闭0或2时,打印出来的log.txt对应的fd的值就是对应的关闭的0或2的值,而当关闭1时,显示器不会显示对应的fd的值。
1 #include <stdio.h>2 #include <sys/types.h>3 #include <sys/stat.h>4 #include <fcntl.h>5 #include <unistd.h>67intmain()8{9//close(0);10//close(1);11//close(2); 12umask(0000);13int fd =open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);//没有指明文件路径,默认在当前路径下,也就是当前进程的工作目录14if(fd<0)15{16perror("open");17return1;18}1920printf("open fd:%d\n",fd);21close(fd);22return0;23}
测试结果:
分析:
所以实际上文件描述符在分配时,会从文件描述符表中的指针数组中,从小到大按照顺序找最小的且没有被占用的fd来进行分配,自然而然关闭0时,0对应存储的地址就会由stdin改为新打开的文件的地址,所以打印新的文件的fd值时,就会出现0。
关闭2也是这个道理,fd为2对应的存储的地址会由stderr改为新打开的文件的地址,所以在打印fd时,也就会出现2了。
文件描述符的分配规则是:
- 当一个进程打开一个新的文件时,系统会在该进程的文件描述符表中寻找当前未使用的最小下标,并将其分配给该文件。
- 当一个进程关闭一个已打开的文件时,系统会将该文件对应的文件描述符表项置为空,并释放其占用的资源。
- 当一个进程复制或重定向一个已打开的文件时,系统会在该进程或目标进程的文件描述符表中寻找当前未使用的最小下标,并将其指向同一个file结构体。
下面是一些示例代码:
- 打开一个新的文件:
#include<stdio.h>#include<stdlib.h>#include<fcntl.h>#include<sys/stat.h>intmain(){// 打开或创建一个新文件int fd =open("test.txt", O_RDWR | O_CREAT | O_TRUNC,0666);if(fd ==-1){// 打开失败,打印错误信息并退出perror("open error");exit(1);}// 打开成功,打印分配到的文档描述符printf("open success, fd = %d\n", fd);// 关闭文档close(fd);return0;}
- 关闭一个已打开的文档:
#include<stdio.h>#include<stdlib.h>#include<fcntl.h>#include<sys/stat.h>intmain(){// 打开或创建一个新文档int fd =open("test.txt", O_RDWR | O_CREAT | O_TRUNC,0666);if(fd ==-1){// 打开失败,打印错误信息并退出perror("open error");exit(1);}// 打开成功,打印分配到的文档描述符printf("open success, fd = %d\n", fd);// 关闭文档int ret =close(fd);if(ret ==-1){// 关闭失败,打印错误信息并退出perror("close error");exit(1);}// 关闭成功,打印关闭信息printf("close success\n");return0;}
文件重定向与dup2系统调用 (内核中更改fd对应的struct file*地址)
重定向命令>和>>的含义和用法
- 重定向命令>和>>都是用来将一个命令的标准输出或错误输出重定向到一个文件中,而不是显示在屏幕上。
- 重定向命令>表示覆盖模式,即如果目标文件已经存在,那么原来的内容会被清空,然后写入新的内容。例如:echo “hello” > log.txt 表示将字符串"hello"写入到log.txt文件中,如果log.txt文件已经存在,那么原来的内容会被覆盖。
- 重定向命令>>表示追加模式,即如果目标文件已经存在,那么新的内容会被追加到原来的内容后面。例如:echo “world” >> log.txt 表示将字符串"world"追加到log.txt文件中,如果log.txt文件已经存在,那么原来的内容会保留。
- 重定向命令>和>>可以指定不同的文件描述符来重定向不同类型的输出。默认情况下,如果不指定文件描述符,那么就是1,表示标准输出。如果要重定向错误输出,就要指定2作为文件描述符。例如:ls -l /etc/passwd /etc/abc > log1.txt 2> log2.txt 表示将ls -l 命令的标准输出重定向到log1.txt文件中,并将错误输出重定向到log2.txt文件中。 测试如下:
我们vim一个abc文件,可以看到abc文件中的内容如下图所示:
然后我们使用 ls > abc 命令之后可以看到abc中的内容已经被清空重定向了
:追加重定向,直接在文件的尾部进行重定向
我们对abc文件进行追加重定向,可以看到,直接在文件的尾巴进行了重定向
重定向原理
简单说将 fd_array 数组当中的元素struct file* 指针的指向关系进行修改,改变成为其它的struct file结构体的地址
所以文件操作重定向的原理是通过改变文件描述符对应的文件描述信息,从而实现改变所操作的文件。文件描述符是一个整数,表示进程和被打开文件的关系,通常有标准输入(0)、标准输出(1)和标准错误(2)三种。重定向可以分为输出重定向、追加重定向和输入重定向三种类型。输出重定向是将本应该打印到显示器的内容输出到了指定的文件中,例如 ls > list.txt;追加重定向是将本应该打印到显示器的内容追加式地输出到了指定的文件中,例如 ls >> list.txt;输入重定向是将本应该从键盘中读取的内容改为从指定的文件中读取,例如 cat < input.txt。在Linux系统中,可以使用dup2系统调用来实现重定向,它可以将一个文件描述符复制到另一个文件描述符,并关闭后者。
dup2函数的功能和参数含义
int dup2(int oldfd, int newfd);
其实这个函数挺绕的,要理解起来需要自己研究一下才行:
通过man手册我们可以对dup2函数进行一些了解:
怎么用?怎么传参数?— 拷贝的整数所表示的内容 — 注意最后只有oldfd保留就可以了!! 就是oldfd把newfd覆盖了 — 一般传参例如:fd 1 (重定向输出)— 只保留了fd
所以dup2函数是一个用于复制文件描述符的系统调用,它的功能是将参数oldfd所指的文件描述符复制到参数newfd所指定的数值,如果newfd已经被打开,则先关闭它,如果newfd等于oldfd,则不做任何操作。dup2函数返回新的文件描述符,或者在出错时返回-1。
- 输出重定向:从原来的输出到屏幕改为输出到文件中,这就叫做输出重定向。 而追加重定向的方式也比较简单,只要将文件打开方式中的O_TRUNC替换为O_APPEND即可。
8intmain()9{10umask(0000);11int fd =open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);//输出重定向
E>12int fd =open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);//追加重定向13if(fd<0)14{15perror("open");16return1;17}1819dup2(fd,1);2021printf("open fd:%d\n",fd);// printf --> stdout 22fprintf(stdout,"open fd:%d\n",fd);// fprintf --> stdout 2324constchar* msg ="hello linux";25write(1,msg,strlen(msg));//向显示器上write 2627close(fd);28return0;29}
- 输出重定向:从原来的键盘中读取数据,改为从文件fd中读取数据,这就叫做输入重定向。 文件log.txt中的内容,作为输入重定向重新输出到显示器中,即使fgets获取的方式是stdin也没有关系,因为我们使用dup2将stdin中的地址改为了文件log.txt的地址
8intmain()9{10umask(0000);13int fd =open("log.txt",O_RDONLY);//输入重定向14if(fd<0)15{16perror("open");17return1;18}1920dup2(fd,0);//由键盘读取改为从fd文件中读取 21char line[64];22while(1)23{24printf("<");25if(fgets(line,sizeof(line),stdin)==NULL)break;26printf("%s",line);27}28}
Linux下面一切皆文件!
不同的硬件的读写方法一定是不一样的,但在OS看来,一切设备和文件都是struct file内核数据结构,在管理对应的硬件时,虽然硬件的管理方法不在OS层,而是在驱动层,这也没有关系,只需要利用struct file结构体中的函数指针,调用对应的硬件的读写方法即可。
终究还是封装的思想!
Linux下一切皆文件是指,Linux系统中的所有资源,无论是硬件设备、普通文件、目录、进程、网络连接等,都可以被抽象为文件,并且可以使用统一的接口来访问和操作。
这样做的好处是,简化了开发者和用户对不同资源的处理方式,提高了系统的灵活性和可扩展性。
这样做的不利之处是,需要在文件系统中挂载每个硬件设备才能使用它们,而且可能会造成一些性能损失。
版权归原作者 阿润菜菜 所有, 如有侵权,请联系我们删除。