文章目录
- 一、利用系统调用进行读写文件操作
-
- [open 打开文件系统调用:](#open 打开文件系统调用:)
- [write 文件写入系统调用:](#write 文件写入系统调用:)
- [read 文件读取系统调用](#read 文件读取系统调用)
- open函数的返回值:
- 二、重定向的原理:

c
1 #include<stdio.h>
2 #include<errno.h>
3 #include<stdlib.h>
4 #include<string.h>
5 int main()
6 {
7 //打开文件fopen
8 FILE* fp=fopen("myfile","w");
9 if(fp==NULL)
10 {
11 perror("打开文件失败:");
12 exit(1);
13 }
14 //
15 const char* str="hello linux!\n";
16 int count=5;
17 while(count--)
18 {
19 fwrite(str,strlen(str),1,fp);//写入文件为什么不含\0?
20
21 }
22
23 //关闭文件
24 fclose(fp);
25 return 0;
26 }
写入文件为什么不含\0?
因为'\0' 是 C 语言内存中识别字符串的 "内部标识",不是文件存储的必需成分;
上面使用的fopen、fclose、fwrite都是C标准库中的函数,我们称之为库函数(libc)。这些f#系列函数都是对系统调用(open、close、read、write)的封装,方便二次开发,增加程序的可移植性,以让更多人使用,增加市场占有率。
一、利用系统调用进行读写文件操作
open 打开文件系统调用:
open 是最核心的文件打开 / 创建系统调用,用于获取文件描述符(fd),后续的 write、read、close 等系统调用都依赖这个文件描述符。它直接与内核交互,支持灵活的文件操作模式和权限控制,是底层文件编程的基础。
函数原型:
c
#include <fcntl.h> // 包含 O_RDONLY、O_CREAT 等标志
#include <unistd.h> // 包含文件描述符相关定义
// 原型 1:打开已存在的文件(或创建文件,需配合 O_CREAT)
int open(const char *pathname, int flags);
// 原型 2:打开并指定文件权限(仅当 flags 包含 O_CREAT 时才需要第三个参数)
int open(const char *pathname, int flags, mode_t mode);
参数 :
1.pathname:文件路径(绝对 / 相对)
绝对路径 :如 /home/user/test.c(完整路径,不受当前工作目录影响);
相对路径 :如 test.c(相对于当前终端的工作目录);
open根据pathname打开文件在哪个路径下呢?
可以使⽤ ls /proc/进程id -l命令查看当前正在运⾏进程的信息。
其中:
• cwd:指向当前进程运⾏⽬录的⼀个符号链接。
• exe:指向启动当前进程的可执⾏⽂件(完整路径)的符号链接。
所以说:打开⽂件,本质是进程打开,所以,进程知道⾃⼰在哪⾥,即便⽂件不带路径,进程也知道。由此OS
就能知道要创建的⽂件放在哪⾥。
2.flags(标记位,其实本质是宏): 打开⽂件时,可以传⼊多个参数选项,⽤下⾯的⼀个或者多个常量进⾏"或"运算,构成flags。
O_RDONLY : 只读打开
O_WRONLY : 只写打开
O_RDWR : 读,写打开上面三个常量,必须指定⼀个且只能指定⼀个。
O_CREAT : 若⽂件不存在,则创建它。需要使⽤mode选项,来指明新⽂件的访问权限。
O_APPEND : 追加写,不清楚文件的内容。
O_TRUNC:清除文件的内容。
3.mode:文件权限(仅 flags 含 O_CREAT 时有效)
类型:mode_t(本质是无符号整数),需用八进制数指定(以 0 开头,如 0644、0755);
含义:指定新创建文件的初始权限,最终权限 = mode & ~umask(umask 是系统默认权限掩码,默认root通常是 0022,普通用户是0002);
可以通过系统调用umask(),修改权限掩码。
4.返回值:
成功:返回新打开的⽂件描述符(fd)(后面重点介绍)
失败:返回-1
在C,语言层面,fopen打开文件的方式有:r、r+、w(文件先会被清空,再写入)、w+、a(文件不会被清空,追加写入)、a+,它们底层都会去调用open系统调用。
write 文件写入系统调用:
write 是 Linux 系统中最基础的文件写入系统调用,用于向文件描述符(文件、管道、socket 等)写入数据。它是底层 I/O 操作的核心,C 标准库的 fwrite 本质上也是对 write 的封装。
1.函数原型:
c
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
2.参数说明:
| 参数 | 含义 |
|---|---|
| fd | 文件描述符(非负整数): 0:标准输入(stdin) 1:标准输出(stdout) 2:标准错误(stderr) 大于 2:打开文件 / 管道 /socket 等返回的描述符 |
| buf | 指向要写入数据的缓冲区 |
| count | 期望写入的字节数 |
3.返回值:
成功:返回实际写入的字节数
失败:返回 -1
注意:无缓冲特性:write 是底层系统调用,无用户态缓冲(区别于 printf/fwrite 的行缓冲 / 全缓冲),调用后直接触发内核 I/O 操作。
系统调用写文件
c
1.#include<stdio.h>
2 #include<errno.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<sys/types.h>
6 #include<sys/stat.h>
7 #include<fcntl.h>
8 #include<unistd.h>
9 //用系统调用写文件
10 int main()
11 {
12 umask(0);//设置系统权限掩码为0
13 //调用系统调用open打开文件
14 int fd=open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);//TRUNC是truncate的简写,核心作用是将文件大小调整到指定长度
15 if(fd<0)//fd表示文件描述符
16 {
17 perror("打卡文件失败:");
18 exit(1);
19 }
20 //写文件
21 const char* msg="hello linux!\n";
22 int cnt=5;
23 size_t len=strlen(msg);
24 while(cnt--)
25 {
26 write(fd,msg,len);//向文件描述符中写入msg缓冲区中的数据,写入的字节数为len
27 }
28 close(fd);
29 return 0;
30 }
read 文件读取系统调用
read 是 Linux/Unix 系统中最基础的文件读取系统调用,用于从文件描述符(文件、管道、socket、标准输入等)读取数据到用户态缓冲区。C 标准库的 fread 本质是对 read 的封装(增加了用户态缓冲)。
1.函数原型:
c
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
2.参数说明:
| 参数 | 含义 |
|---|---|
| fd | 文件描述符(非负整数): 0:标准输入(stdin) 1:标准输出(stdout,不支持读取) 2:标准错误(stderr,不支持读取) 大于 2:打开文件 / 管道 /socket 等返回的描述符(需确保以 "可读" 模式打开) |
| buf | 指向用户态缓冲区(用于存储读取到的数据),必须是可写内存(不能是 const),且需提前分配足够空间 |
| count | 期望读取的最大字节数(size_t 是无符号整数,需不超过缓冲区实际大小,避免内存溢出) |
3.返回值:
成功:返回实际读取的字节数(ssize_t带符号整数,可表示 - 1/0 / 正数),分三种情况:
实际读取字节数 = count:缓冲区已满或数据足够;
实际读取字节数 < count:已到达文件末尾(EOF),或管道 /socket 中数据不足,或被信号中断;
返回 0:到达文件末尾(EOF)(无更多数据可读取)。
失败:返回 -1,并设置 errno 表示错误类型(需包含 <errno.h> 查看)。
注意:read的无缓冲特性:read 是底层系统调用,无用户态缓冲(区别于 fgets/fread),调用后直接从内核态读取数据到用户缓冲区。
系统调用读文件:
c
1 #include<stdio.h>
2 #include<errno.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<sys/types.h>
6 #include<sys/stat.h>
7 #include<fcntl.h>
8 #include<unistd.h>
9
10 //用系统调用读文件
11 int main()
12 {
13 //打开文件
14 int fd=open("log.txt",O_RDONLY);
15 if(fd<0)
16 {
17 perror("打开文件失败:");
18 exit(1);
19 }
20 //读取文件
21 char buf[1024];// 定义缓冲区(栈上分配,大小可控)
22 while(1)
23 {
24 ssize_t s=read(fd,buf,strlen(buf));//从文件描述符中读取数据到buf缓冲区中,每次读取strlen(buf)字节。
25 if(s>0)
26 {
27 printf("%s",buf);
28 }
29 else break;
30 }
31 //关闭文件
32 close(fd);
33 return 0;
34 }
open函数的返回值:
前面提到open函数的返回值是一个文件描述符(File Descriptor, fd),本质是一个非负整数。
1.open成功返回时
返回一个大于等于 3 的非负整数,在Linux下,进程默认情况下会有3个⽂件描述符,分别是标准输⼊0,标准输出1,标准错误2,0,1,2对应的物理设备⼀般是:键盘,显⽰器,显⽰器。这个整数是内核分配给该文件的唯一标识,后续对文件的读写(read()/write())、关闭(close())等系统调用操作都需要通过这个文件描述符来完成。
2.失败时的返回值
固定返回 -1,同时内核会设置全局变量 errno 来标识具体的错误原因(需要包含 <errno.h> 头文件才能使用)。
文件描述符(fd)的原理:

我们要读取一个文件的内容(log.txt),首先得打开它,而文件是储存在底层硬件磁盘上,上层用户要打开它,必须通过操作系统,以前在C语言中,调用fopen打开文件,其实这是对系统调用open做的封装,底层还是通过调用open打开的文件,操作系统打开文件时,会在内核创建struct_file数据结构,包含文件的权限、读写位置、读写方法、缓冲区等属性,还会用链表(list head双链表)将所有struct file连接起来。这是因为操作系统是一款管理底层硬件的软件,要管理就是"先描述(struct_file),再组织(链表)",管理这些打开的文件,就转化为了对链表的增删查改。而进程执行open系统调⽤,必须让进程和⽂件关联起来。每个进程都有⼀个指针*files,指向⼀张表files_struct,该表最重要的部分:包含⼀个指针数组,每个元素都是⼀个指向打开⽂件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
文件读写操作的原理:
open调用 :用户层进程执行open调用时,操作系统创建struct_file,在当前进程的文件描述符表中找当前未使用的最小下标,填入struct_file地址,关联进程和文件。
文件描述符的分配原则 :最小的,没有被使用,作为新的fd给用户。
read调用 :read调用时,操作系统根据文件描述符索引数组找到对应文件,将磁盘文件内容预加载到缓冲区,再将缓冲区数据拷贝到用户层的buffer。
write调用 :write时,先根据文件描述符找到对应文件,将用户空间数据拷贝到文件缓冲区,操作系统定期将缓冲区数据刷到磁盘。
文件修改操作 :对文件内容做任何修改,都要先将文件加载到内核缓冲区,在内存修改后再写回磁盘,本质是磁盘到内存的拷贝。
二、重定向的原理:
先来看一个现象:
c
1 #include<stdio.h>
2 #include<errno.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<sys/types.h>
6 #include<sys/stat.h>
7 #include<fcntl.h>
8 #include<unistd.h>
9 int main()
10 {
11 close(1);
12 int fd=open("log.txt",O_CREAT | O_WRONLY ,0666);
13 if(fd<0)
14 {
15 perror("打开文件失败");
16 exit(1);
17 }
18 printf("hello linux\nfd:%d\n",fd);
19 fflush(stdout);//强制刷新缓冲区
20
21 close(fd);
22 return 0;
23 }

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件log.txt当中,其中,fd=1。这种现象叫做输出重定向。
常见的重定向有:
| 符号 | 作用 | 底层原理 |
|---|---|---|
| > | 覆盖式输出重定向 | 打开文件时使用O_TRUNC(截断文件,清空原有内容) |
| >> | 追加式输出重定向 | 打开文件时使用O_APPEND(追加模式,写入到文件末尾) |
| < | 输入重定向 | 打开文件时使用O_RDONLY |
>、>>、<是命令解释器(Shell) 定义的语法规则,作用是告诉 Shell:"在执行后续程序前,先修改它的 IO 指向";它们的底层是通过系统调用open和dup2实现的。
重定向的核心操作 :修改文件描述符的指向
1.关闭默认的 1 号文件描述符(断开与终端的连接);
2.打开目标文件(比如 log.txt),操作系统会为这个文件分配一个新的文件描述符(通常刚好是刚关闭的 1);
3.后续程序向 1 号描述符写入的所有数据,都会被操作系统转发到新打开的文件中。
举个通俗例子 :
默认情况下,1 号接口接的是 "终端水管",程序的输出会顺着水管流到终端屏幕;重定向就是把 1 号接口的水管拔下来,重新接到 "文件水管" 上,输出自然就流到文件里了。
重定向的本质 :更改文件描述符表的指向。
思考:
上图中1号文件描述符指向了log.txt文件,那标准输出文件的struct file结构体是否会被释放?
在操作系统内核中,一个文件可以被多个进程打开,struct file中存在一个ref_cnt变量,记录指向该文件的进程数量,当该文件被重定向时,ref_cnt变量会-1,当有新的新的文件描述符指向struct file时,ref_cnt会+1,当struct file中的ref_cnt减到0时,struct file结构体才会被释放。
标准错误重定向:
c
1 #include<iostream>
2 #include<stdio.h>
3 int main()
4 {
5 std::cout << "hello cout" << std::endl;
6 printf("hello printf\n");
7
8 std::cerr<<"hello cerr"<<std::endl;
9 fprintf(stderr,"hello stderr\n");
10 return 0;
11 }

g++ stream.cc编译代码,然后将运行后的内容重定向输出到log.txt文件,即./a.out > log.txt,结果应该是运行的结果都输出到log.txt文件。但是标准错误没有输出到log.txt文件,这是为什么呢?其实./a.out > log.txt等价于./a.out 1 > log.txt, 也就是1号文件描述符重定向到log.txt,但是标准错误还是对应的2号文件描述符,标准错误对应的物理硬件还是显示器,所以标准错误最终打印到屏幕上。
既然标准输出和标准错误对应的物理硬件都是显示器,那为什么不让标准输出和标准输入合二为一呢?这是因为我们可以通过重定向能力,把常规消息和错误消息进行分离。

将stderr和stdout进行分离:

如果stderr和stdout打印到同一个文件呢?

./a.out 1>log.txt 2>&1 是一个将标准输出和标准错误都重定向到同一个文件的经典写法,1>log.txt:将标准输出(文件描述符 1)重定向到 log.txt 文件(覆盖原有内容);2>&1:将标准错误(文件描述符 2)重定向到与标准输出(1)相同的目标(也就是 log.txt)。

dup2系统调用:
它是实现重定向的核心工具,dup2(fd1, fd2) 的作用是让文件描述符 fd2 完全 "继承" fd1的指向,最终让两个文件描述符指向同一个文件 / 设备。
1.函数原型:
bash
#include <unistd.h>
int dup2(int oldfd, int newfd);
2.参数 :
oldfd:已有、指向目标文件 / 设备的文件描述符(比如打开文件后得到的 fd);
newfd:要被 "覆盖" 的文件描述符(比如 1 号 stdout、0 号 stdin)。
3.返回值 :
成功返回 newfd,失败返回 -1 并设置 errno。
示例:
实现从键盘获取内容,标准输出(stdout)重定向到 log.txt 文件
bash
1 #include<stdio.h>
2 #include<errno.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<sys/types.h>
6 #include<sys/stat.h>
7 #include<fcntl.h>
8 #include<unistd.h>
9
10 int main()
11 {
12 int fd=open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);
13 if(fd<0)
14 {
15 perror("open fail");
16 exit(1);
17 }
18
19 dup2(fd,1);//将1号FD(stdout)重定向到log.txt
20 close(fd);//关闭多余的fd,fd已被1号复用,无需保留。
21 while(1)
22 {
23 char buffer[1024];
24 ssize_t s=read(0,buffer,sizeof(buffer)-1);// 留1个字节给'\0'
25 if(s<0)
26 {
27 perror("read fail");
28 exit(2);
29 }
30 if(s==0)// 处理输入结束(Ctrl+D)
31 {
32 printf("输入结束!\n");
33 fflush(stdout);
34 break;
35 }
36 buffer[s]='\0';//手动添加字符串结束符,避免printf乱码
37 printf("%s",buffer);
38 fflush(stdout);//强制刷新缓冲区,确保内容立即写入文件
39 }
40 return 0;
41 }

在自定义minishell中添加重定向功能:
在原有minishell的基础上添加重定向功能。
思路:
1.重定向处理思路:
获取到用户输入的命令行信息后,在命令行解析前,先进行重定向分析。即将命令行"ls -a -l > myfile.txt"拆分成:"ls -a -l" 和"myfile.txt",再判定重定向方式。
2.重定向分析接口编写:RedirCheck(实现命令行的拆分)
2.1 定义重定向方式的类型 ,包括无重定向(NONE_REDIR,值为 0)、输入重定向(INPUT_REDIR,值为 1)、输出重定向(OUTPUT_REDIR,值为 2)、输追加重定向(APPEND_REDIR,值为 3);默认重定向类型为无重定向,默认文件名为空。每次操作前清空重定向类型和文件名。
2.2 使用 while 循环从命令行字符串末尾向前查找重定向符号(> 、>> 、< ),若找到重定向符号,将其置为 '\0',以此分隔左右两部分,左侧为要执行的命令,右侧为文件名。
2.3 编写Trimspace函数,去除文件名前的空格,函数第一个参数为要处理的字符串,第二个参数为引用的下标,用于定位文件名起始位置。包含头文件 ctype.h,使用isspace 函数判断字符是否为空格,若为空格则将下标后移,直到不是空格为止。
3.子进程做重定向处理:
使用打开文件方式+dup2修改文件描述符所对应的指针指向位置,实现重定向的底层处理。
minishell代码链接 :minishell
