【Linux】文件操作&&重定向原理

文章目录

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标准错误20,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

相关推荐
若风的雨2 小时前
安全与验证模块设计方案
linux·安全
2603_949462102 小时前
Flutter for OpenHarmony社团管理App实战:消息中心实现
android·javascript·flutter
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.2 小时前
Haproxy ACL实战:精准分流与访问控制
运维
Eiceblue2 小时前
.NET框架下Windows、Linux、Mac环境C#打印PDF全指南
linux·windows·.net
andr_gale2 小时前
08_flutter中如何优雅的提前获取child的宽高
android·flutter
RockHopper20253 小时前
解读数字化生产运行系统的裁决机制
运维·系统架构·智能制造·isa-95·isa-88
试试勇气3 小时前
Linux学习笔记(十三)--文件系统
linux·笔记·学习
guizhoumen3 小时前
2026年建站系统推荐及选项指南
大数据·运维·人工智能
yingdonglan3 小时前
鸿蒙跨端Flutter学习——GridView高级功能
linux·运维·windows