目录
2、文件描述符具体是什么?为什么后续访问文件的系统调用都要通过fd来操作?
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家
一、知识铺垫
(1)文件 = 内容 + 属性
(2)访问文件之前,都得先打开该文件。修改文件都是通过执行代码的方式完成修改。
(3)打开文件前提要文件必须加载到内存中
(4)由谁打开文件?进程打开文件。
(5)一个进程可以打开多少文件?可以打开多个文件
(6)一定时间内,系统中会存在多个进程,也可能同时存在更多的被打开文件,OS要不要管理多个被进程打开的文件呐?答案是肯定要管理的。如何管理呐?答案是先描述在组织。
(所以内核中一定要有描述被打开文件的结构体,并用其定义对象)。
(7)进程和文件的关系:结构体之间的关系,struct task_struct 和 struct XXX
(8)系统中是不是所有的文件都被进程打开了?答案是:并不是,那些没有被打开的文件是被存储磁盘中的,所以也叫磁盘文件。
二、回顾一下之前C语言的文件操作,并对比重定向
cpp#include<stdio.h> int main() { FILE *fp = fopen("./log.txt","w"); if(fp == NULL) { perror("fopen"); return 1; } const char* str = "hello file\n"; fputs(str,fp); fclose(fp); return 0; }
1、w选项与输出重定向(>)
以" w "选项打开文件,是对文件进行写操作,但是打开前会将文件原有内容清空。而重定向(" > ")也是会将文件原有内容清空,因为重定向之前需要将文件打开,而打开这个操作就会将文件内容清空:
2、a选项与追加重定向(>>)
fopen以"a"选项打开文件,是追加的方式进行写,即在文件原有内容的末尾接着写,这与追加重定向功能一样:
cpp#include<stdio.h> int main() { FILE *fp = fopen("./log.txt","a"); if(fp == NULL) { perror("fopen"); return 1; } const char* str = "hello file\n"; fputs(str,fp); fclose(fp); return 0; }
还有其他选项可以查手册。
3、熟悉一下读写操作
cpp#include<stdio.h> #include<string.h> #define FILENAME "log.txt" //练习读写操作 int main() { FILE* fp = fopen(FILENAME,"w"); if(fp == NULL) { perror("fopen"); return 1; } const char* msg = "hello HF"; int cnt = 6; while(cnt) { int n = fwrite(msg,strlen(msg),1,fp); printf("write %d block\n",n); cnt--; } fclose(fp); return 0; }
4、练习读操作:
cpp#include<stdio.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #define FILENAME "log.txt" //练习读 int main() { FILE* fp = fopen(FILENAME,"r"); if(fp == NULL) { perror("fopen"); return 1; } char buffer[64]; while(1) { char* r = fgets(buffer,sizeof(buffer),fp); if(!r) break;//返回NULL则终止读 printf("%s\n",buffer); } return 0; }
三、什么叫做当前路径?当前路径与文件创建的关系?
当前路径指进程启动时所在的工作目录。进程启动时,会自动记录自己启动时的所在的目录,可通过指令查看:(ls /proc/进程pid -l)
当前路径与文件创建的关系:以前都以为文件是默认创建在可执行程序的同级目录,实则不然,文件是默认创建在进程的工作目录下。如果我们在创建文件之前,修改进程的工作目录,那么文件也会创建到修改后的工作目录下:
修改进程的工作目录的接口:
参数就是要修改的工作目录。
cpp#include<stdio.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #define FILENAME "HF.txt" //修改进程的工作目录 int main() { int i = chdir("/root/study/linux-learning"); if(i) { printf("转换失败\n"); } FILE* fp = fopen(FILENAME,"w"); if(fp == NULL) { perror("fopen"); return 1; } const char* msg = "hello HF"; int cnt = 6; while(cnt) { int n = fwrite(msg,strlen(msg),1,fp); printf("write %d block\n",n); cnt--; } fclose(fp); return 0; }
四、访问文件的系统调用
1、程序默认打开的文件流
2、常见的读写函数:
cpp#include<stdio.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #define FILENAME "log.txt" //常见的写函数 int main() { printf("hello printf\n"); fprintf(stdout,"hello fprintf\n");//将数据输出到标准输出中(stdout显示器设备); fputs("hello fputs\n",stdout);//也是将数据输出到标准输出中,但不能像fprintf那样支持格式化输出 const char* msg = "hello fwrite\n"; fwrite(msg,1,strlen(msg),stdout); return 0; }
cpp#include<stdio.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #define FILENAME "log.txt" int main() { //fscanf char buffer[64]; fscanf(stdin,"%s",buffer);//从标准输出(键盘)中读取数据放到buffer中,空格和换行符作为分隔符 printf("%s\n",buffer); return 0; }
3、open系统调用:
访问文件不仅仅有C语言的文件接口,OS还必须提供对应的访问文件的系统调用,就是open系列的系统调用:
4、一个小细节:用位图传参:
cpp#include<stdio.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #define FILENAME "log.txt" //用位图传参 #define ONE 1 #define TWO (1<<1) #define THREE (1<<2) #define FOUR (1<<3) #define FIVE (1<<4) void myPrint(int flag) { if(flag & ONE) printf("1"); if(flag & TWO) printf("2"); if(flag & THREE) printf("3"); if(flag & FOUR) printf("4"); if(flag & FIVE) printf("5"); printf("\n"); } int main() { myPrint(ONE); myPrint(TWO); myPrint(ONE | TWO); myPrint(THREE | FOUR | FIVE); myPrint(FIVE); return 0; }
5、open参数解析:
(1)参数一:pathname,是要打开或者创建的文件路径名(绝对路径/相对路径)
(2)参数二:flags,标志位,表示打开文件的方式,具体值是宏定义(可查看手册),传参方式类似于第4点的位图传参方式,可以通过按位或(|)设置多个标志。
注意:使用这些标志位需要包含头文件:
<fcntl.h>
三个基本标志位:
标志 作用 O_RDONLY 只能读取,不能写入,若进行写操作会返回 EBADF
错误O_WRONLY 只能写入,不能读取,需配合 O_CREAT
创建新文件O_RDWR 可同时读取和写入,写入可能覆盖原有内容,需控制偏移量 其他标志如下:
标志 作用 O_CREAT 如果文件不存在,则创建它(需配合第三个参数 mode
使用)。O_EXCL 与 O_CREAT
联用,若文件已存在则返回错误(可用于避免文件被意外覆盖)。O_TRUNC 若文件存在且为可写模式,将其长度截断为 0(即打开文件前先清空文件内容)。 O_APPEND 追加写,写入时始终追加到文件末尾(自动将文件偏移量设置到文件末尾)。 O_NONBLOCK 以非阻塞模式打开文件(用于 I/O 多路复用,如网络编程)。 (3)参数三:mode,用于指定新建文件的权限,仅当使用
O_CREAT
或O_TMPFILE
标志时生效。有时候,如果不指定mode参数,那么创建的文件的权限可能会乱码:
cpp#include<stdio.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #include<fcntl.h> #define FILENAME "log.txt" //使用open int main() { int fd = open("HF.txt",O_WRONLY | O_CREAT); if(fd == -1) { perror("open"); return 1; } return 0; }
如图会发现创建的文件的权限位乱码的,所以此时我们需要使用第三个参数code解决:
但受系统权限掩码的限制,会导致创建的文件的权限与我们设置的权限不一样,此时需要提前使用一个系统调用:umask(0),头文件:
<sys/stat.h>
解释:用于设置当前进程的文件创建掩码(file creation mask)为 0。文件创建掩码是一个位掩码,用于在创建文件或目录时屏蔽某些权限位,从而控制新创建文件的默认权限。
此时就与我们设置的权限一样了。
6、写文件的系统调用:write
参数解析:
参数名称 数据类型 描述 fd int 文件描述符 ,指向已打开的文件、管道、套接字或设备(如标准输入stdin对应 fd = 0;标准输出stdout对应 fd = 1;标准错误stderr对应 fd = 2)。 通过 open
系统调用获取,用于标识写入目标。buf const void * 写入缓冲区指针,它是一个指向用户空间缓冲区的指针,这个缓冲区里存储着准备写入的数据。数据的传输方向是从 用户空间(buf)到内核空间(fd 对应的设备或文件)。 可以是字符数组、结构体或其他数据类型,需确保内存访问权限合法。 count size_t 要写入的字节数 ,指定从 buf
中读取的最大数据量。 实际写入字节数可能小于count
(如遇到文件末尾、磁盘空间不足或权限限制)。
cpp#include<stdio.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #include<fcntl.h> #include<sys/stat.h> #define FILENAME "log.txt" int main() { umask(0); int fd = open("HF.txt",O_WRONLY | O_CREAT,0666); const char* str = "hello write\n"; write(fd,str,strlen(str)); return 0; }
注意:
7、关闭文件的系统调用:close
五、文件描述符(fd)
1、认识文件描述符
这是一个及其重要的概念,文件描述符也就是open函数的返回值,我们先看看值是什么?
cpp#include<stdio.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #include<fcntl.h> #include<sys/stat.h> #define FILENAME "log.txt" //认识文件描述符 int main() { int fd1 = open("HF1.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); int fd2 = open("HF2.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); int fd3 = open("HF3.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); int fd4 = open("HF4.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); int fd5 = open("HF5.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); 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; }
可以看到值其实就是整形,但为什么是从3开始呐?
因为0,1,2端口已经默认被三个标准占用了:
标准输入(stdin):0
标准输出(stdout):1
标准错误(stderr):2
由上述知识我们可以知道,C语言的相关文件接口,本质就是封装了各个系统调用,主要是为了保证自己的跨平台性。
2、文件描述符具体是什么?为什么后续访问文件的系统调用都要通过fd来操作?
上面我们知道,进程要管理打开的文件需要先描述后组织。
(1)首先task_struct中存在一个成员变量(struct file_struct *files),这个成员指向的结构体(struct file_struct)里面存在一个成员,该成员表示进程打开的文件描述符表。
(2)所谓文件描述符表也就是一个数组,其数组类型为(struct file** fd_array[ ]),这是一个二级指针,其内容的类型为(struct file*)
(3)struct file是文件结构体,里面包含了文件属性、方法集、文件运行时的状态信息、操作函数和资源引用等等信息(如下图),被称为文件操作的 "控制器"。
(4)而open返回值fd(文件描述符)就是这个文件描述符表的下标,有了这个下标,我们就可以找到下标对应的struct file,从而就可以操作这个文件,所以后续访问文件的系统调用都要通过fd来操作的。
3、一切皆文件
4、文件描述符表的分配规则以及利用规则实现重定向
(1)文件描述符表的分配规则
文件描述符表的分配规则:会叫最小的没有被使用的下标,分配给最新打开的文件。
输出重定向的现象:
(2)改变重定向的系统调用(dup2)
我们先学习dup2系统调用,参数解析如下:
(1)oldfd:已存在的、有效的文件描述符,指向一个已打开的文件、设备或套接字。
|-----------------------------------------------------------|
| 若oldfd
无效(如未打开或已关闭),dup2()
返回-1
并设置errno=EBADF
|(2)newfd:新绑定的文件描述符,
dup2()
会将文件对象以前的文件描述符oldfd解绑,然后
与newfd进行绑定
。|---------------------------------------------------------------------------------------|
| 若newfd
未打开 : 直接将newfd
指向oldfd
对应的struct file
对象。 |
| 若newfd
已打开 : 先关闭newfd
(减少其原struct file
的引用计数),再复制oldfd
的文件对象到newfd
。 |
| 若newfd == oldfd
: 不执行任何操作,直接返回newfd
(避免自我关闭)。 |
(3)dup2使用场景
重定向到文件:
cpp//使用dup2 int main() { int fd = open("newfile",O_WRONLY|O_CREAT|O_TRUNC,0666); dup2(fd,1);//将newfile文件与标准输出进行绑定 //这样printf就会默认向上述文件进行输出 printf("hello newfile\n"); return 0; }
从文件读取内容到数组:
cppint main() { int fd = open("newfile",O_RDONLY,0666); dup2(fd,0);//将文件的文件描述符与标准输出进行绑定 char buffer[1024]; while(1) { //默认情况,stdin会从键盘中读取,若键盘不输入,是会发生阻塞的 char* s = fgets(buffer,sizeof(buffer),stdin);//此时stdin会默认从文件中读取 if(s==NULL)break; printf("file content:%s",buffer); } return 0; }
5、给自定义shell增加重定向功能
六、缓冲区问题:
1、简单介绍
缓冲区其实是一块内存区域,目的是用来提高使用者的效率(空间换时间)。
比如从云南到北京运送货物,总共需要运送100kg,如果一次运送10kg,需要来回10次,这样花费的时间就非常多;如果我用比较大的运输机一次就能运送100kg,这样就用运送一次,大大提高了效率。
2、为什么使用缓冲区能提高效率?
注意:平时我们所说的,包括这里即将减少的缓冲区都是语言层面的缓冲区(比如C语言里面的缓冲区),与OS内核中的缓冲区没有关系。
为什么使用语言层面的缓冲区能提高效率?
结合上述运送物资的例子,我们知道通过系统调用访问OS是需要有很大开销的,如果我们语言层面不设置缓冲区,那么来一点数据就送给OS,又来一点数据又会访问OS,这样就会多出很多开销,但如果我们在语言层设置一个缓冲区,让需要存储的数据线一点一点累积保存到缓冲区,达到一定的空间后,我们一次性传输给OS,这样访问OS的次数就大大减少了,从而就提高了效率。
3、缓冲区在哪里?
缓冲区是在FILE结构体中,也就是由FILE结构体来维护缓冲区:
缓冲区常见字段:
cppstruct _IO_FILE { // 基础文件描述符 int _fileno; // 缓冲区指针与状态 char* _IO_read_ptr; // 读缓冲区当前位置 char* _IO_read_end; // 读缓冲区结束位置 char* _IO_read_base; // 读缓冲区起始位置 char* _IO_write_base; // 写缓冲区起始位置 char* _IO_write_ptr; // 写缓冲区当前位置 char* _IO_write_end; // 写缓冲区结束位置 char* _IO_buf_base; // 缓冲区基址 char* _IO_buf_end; // 缓冲区结束地址 // 缓冲区状态标志 int _IO_write_base; // 写缓冲区起始位置(重复字段,实际为标志位) unsigned _flags; // 缓冲区标志(如是否全缓冲、行缓冲等) unsigned _IO_file_flags; // 文件状态标志 // 缓冲区大小与类型 int _IO_buf_size; // 缓冲区大小 int _mode; // 读写模式 // ... 其他字段(省略) };
核心字段:
4、用代码证明缓冲区的存在
cpp//证明缓冲区的存在 int main() { //使用系统调用 const char* s1 = "hello write\n"; write(1,s1,strlen(s1)); //使用C语言接口 const char* s2 = "hello fprintf\n"; fprintf(stdout,"%s",s2); const char* s3 = "hello fwrite\n"; fwrite(s3,strlen(s3),1,stdout); fork(); return 0; }
如果我们直接运行那么就会正常打印,因为显示器是行刷新(即写完一行就刷新数据(\n))
但如果我们重定向到某个文件,会发现一个奇怪的现象:
C语言接口的内容会存在两份,而系统调用的接口内容只有一份
因为我们使用了重定向,重定向的刷新策略是全缓存刷新(即缓冲区满了才刷新数据),但很显然代码中的两条内容是塞不满缓冲区的,所以此时会一直等待,最后会遇到fork创建子进程,而刷新数据也属于修改数据的一种方式,父子进程中任意一个进程修改共享数据时,都会进行写实拷贝,最后进程结束,缓冲区强迫刷新,父子进程都会向文件中刷新数据,所以C语言接口的数据会存在两份,而系统调用接口write会直接将内容存在系统内部的缓冲区,此时内容与父子进程无关,所以只有一份数据。
其次printf、scanf等等函数的格式化输出也与缓冲区有关,可以搜索了解了解。
七、模拟实现文件操作的常用接口(有缓冲区和无缓冲区版本)
1、无缓冲区版本
mystdio.h
cpp#pragma once #include<stdio.h> typedef struct _myFILE { int fileno; }myFILE; myFILE* my_fopen(const char* pathname,const char* mode); int my_fwrite(myFILE* fp,const char* fs,int size); //int my_fread(); void my_fclose(myFILE* fp);
mystdio.c
cpp#include "mystdio.h" #include <string.h> #include <sys/stat.h> #include <sys/types.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> myFILE *my_fopen(const char *pathname, const char *mode) { int flag = 0; if (strcmp(mode, "r") == 0) { flag |= O_RDONLY; } else if (strcmp(mode, "w") == 0) { flag |= (O_CREAT | O_WRONLY | O_TRUNC); } else if (strcmp(mode, "a") == 0) { flag |= (O_CREAT | O_WRONLY | O_APPEND); } else { return NULL; } int fd = 0; if (flag & O_WRONLY) { umask(0); fd = open(pathname, flag, 0666); } else { fd = open(pathname, flag); } if (fd < 0) return NULL; myFILE *fp = (myFILE *)malloc(sizeof(myFILE)); if (fp == NULL) return NULL; fp->fileno = fd; return fp; } int my_fwrite(myFILE* fp,const char* s,int size) { return write(fp->fileno,s,size); } void my_fclose(myFILE* fp) { close(fp->fileno); free(fp); }
filetest.c
cpp#include"mystdio.h" #include<string.h> const char* filename = "./log.txt"; int main() { myFILE* fp = my_fopen(filename,"w"); if(fp == NULL) return 1; const char* s = "hello myflie\n"; my_fwrite(fp,s,strlen(s)); my_fclose(fp); return 0; }
2、有缓冲区版本
mystdio.c
cpp#include "mystdio.h" #include <string.h> #include <sys/stat.h> #include <sys/types.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> myFILE *my_fopen(const char *pathname, const char *mode) { int flag = 0; if (strcmp(mode, "r") == 0) { flag |= O_RDONLY; } else if (strcmp(mode, "w") == 0) { flag |= (O_CREAT | O_WRONLY | O_TRUNC); } else if (strcmp(mode, "a") == 0) { flag |= (O_CREAT | O_WRONLY | O_APPEND); } else { return NULL; } int fd = 0; if (flag & O_WRONLY) { umask(0); fd = open(pathname, flag, 0666); } else { fd = open(pathname, flag); } if (fd < 0) return NULL; myFILE *fp = (myFILE *)malloc(sizeof(myFILE)); if (fp == NULL) return NULL; fp->fileno = fd; fp->cap = SIZE; fp->pos = 0; fp->flush_mode = LINE_FLUSH; return fp; } void my_fflush(myFILE* fp) { if(fp->pos == 0) return; write(fp->fileno,fp->outbuffer,fp->pos); fp->pos = 0; } int my_fwrite(myFILE* fp,const char* s,int size) { //向缓冲区写入 memcpy(fp->outbuffer+fp->pos,s,size); fp->pos += size; if((fp->flush_mode & LINE_FLUSH) && fp->outbuffer[fp->pos-1] == '\n') { my_fflush(fp); } else if((fp->flush_mode & LINE_FLUSH)&&fp->pos == fp->cap) { my_fflush(fp); } return size; //return write(fp->fileno,s,size); } const char* toString(int flag) { if(flag & NONE_FLUSH) return "None"; else if(flag & LINE_FLUSH)return "Line"; else if(flag & FULL_FLUSH)return "FULL"; return "err"; } void DebugPrint(myFILE* fp) { printf("outbuffer:%s\n",fp->outbuffer); printf("fd:%d\npos:%d\ncap:%d\nflush_node:%s\n",fp->fileno,fp->pos,fp->cap,toString(fp->flush_mode)); } void my_fclose(myFILE* fp) { my_fflush(fp); close(fp->fileno); free(fp); }
mystdio.h
cpp#pragma once #include<stdio.h> #define SIZE 4096//缓冲区大小 #define NONE_FLUSH (1<<1)//无自动刷新 #define LINE_FLUSH (1<<2)//行刷新 #define FULL_FLUSH (1<<3)//全刷新 typedef struct _myFILE { //char inbuffer[]; char outbuffer[SIZE]; int pos; int cap; int flush_mode; int fileno; }myFILE; myFILE* my_fopen(const char* pathname,const char* mode); int my_fwrite(myFILE* fp,const char* fs,int size); //int my_fread(); void my_fflush(myFILE* fp); void DebugPrint(myFILE* fp); void my_fclose(myFILE* fp);
filetest.c
cpp#include "mystdio.h" #include <string.h> #include <unistd.h> const char *filename = "./log.txt"; int main() { myFILE *fp = my_fopen(filename, "w"); if (fp == NULL) return 1; int cnt = 5; char buffer[64]; while (cnt) { snprintf(buffer, sizeof(buffer), "helloworld,hellobit,cnt:%d", cnt--); my_fwrite(fp, buffer, strlen(buffer)); DebugPrint(fp); sleep(2); } my_fclose(fp); return 0; }