跳到主要内容 Linux 基础 IO 详解 | 极客日志
C
Linux 基础 IO 详解 讲解 Linux 基础 IO。涵盖文件概念、C 语言文件 IO 接口回顾、系统调用 open/read/write/dup2 的使用、文件描述符机制、重定向原理及实现、一切皆文件(VFS)的理解、内核与用户层缓冲区区别,以及简单 libc 库的设计。
微码行者 发布于 2026/3/23 更新于 2026/4/16 7.9K 浏览Linux 基础 IO 详解
本文介绍 Linux 基础 IO。首先回顾 C 语言中的文件 IO 接口,引入文件系统相关调用,理解文件描述符及重定向本质。在此基础上为自主命令行解释器新增重定向功能,理解'一切皆文件'、内核缓冲区与文件缓冲区的区别,并设计简单的 libc 库。
1. 理解文件
通过之前的学习知道文件=文件内容 + 文件属性 。当文件内的内容为空时文件也是要占据相应的内存空间,但是文件原本是存储在磁盘当中的,当用户进行相应的文件操作实际上的流程是怎么样的呢?
我们知道在冯诺依曼体系当中 CPU 是只和内存打交道的,那么原本存储在磁盘当中的文件要进行对应的操作就需要先从磁盘加载到内存上;在该过程当中实现是由操作系统创建对应的进程之后由进程来实现以上的操作,这些进程本质上就是相应的系统调用 ,而在语言的层面上是通过封装相应的系统调用来实现出函数提供给用户使用;例如在 C 当中的fopen、fwrite、fclose 等文件相关的操作。
但用户可能会同时打开多个文件,那么这就使得需要对打开的文件进行管理,这其实是由操作系统来实现的。在本篇当中本质上了解的是'内存'级的文件,而要了解文件是如何在磁盘当中进行读取的需要等到之后的文件系统篇章中。
2. 接口回顾
通过之前的学习我们已经了解了 C 当中提供的文件相关的操作函数,那么接下来就先复习这些函数的使用,之后再通过这些函数来引入 Linux 当中文件相关的系统调用。
当以下使用 fopen 打开一个在当前的路径下不存在的文件,我们知道当使用 r 方式是会出现报错的,这时需要使用 w 方式这时就会在当前的路径创建对应文件名的文件。
#include <stdio.h>
int main () {
FILE* fp=fopen ("text.txt" ,"w" );
if (fp==NULL ) {
perror ("fopen error!" );
}
if (fp!=NULL )printf ("open success!\n" );
fclose (fp);
return 0 ;
}
以上代码编译为可执行程序之后执行的结果如下所示:
并且在当前的路径下创建出了名为 text.txt 的文件。
在此接下来就可以使用 fwrite 和 fread 来对以上创建的 text.txt 文件进行读写的操作,例如以下的代码:
#include <stdio.h>
#include <string.h>
int main () {
FILE* fp=fopen ( , );
(fp== ) {
( );
}
*str1 = ;
cnt= ;
(cnt--) {
(str1, (str1), ,fp);
}
(fp);
;
}
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
"text.txt"
"w"
if
NULL
perror
"fopen error!"
const
char
"hello world!\n"
int
5
while
fwrite
strlen
1
fclose
return
0
以上代码就是使用 fwrite 来将 str1 字符串写入到 text.txt 文件当中,在此写入 5 次,运行以上代码编译生成的可执行程序之后再查看 text.txt 就会发现内容确实被写入了。
以上向 text.txt 当中输入了对应的字符串,那么接下来试着使用fread 来读取 text.txt 当中的内容,例如以下的代码:
#include <stdio.h>
#include <string.h>
int main () {
FILE* fp=fopen ("text.txt" ,"r" );
if (fp==NULL ) {
perror ("fopen error!" );
}
const char *str1 ="hello world!\n" ;
char ch[1024 ];
while (1 ) {
size_t s=fread (ch,1 ,strlen (str1),fp);
if (s>0 ) {
ch[s]=0 ;
printf ("%s" ,ch);
}
if (feof (fp)) {
break ;
}
}
fclose (fp);
return 0 ;
}
以上的代码当中就创建了一个临时的数组来存放从文件当中读取的数据,之后每次使用 fread 从 text.txt 当中读取 str1 长度的字符,fread 函数的返回值就是读取到的字符的个数。
通过以上简单的代码就复习了 C 当中提供进行文件操作的函数,我们知道在使用 fopen 打开文件的时候返回值是对应的文件流指针。而当未进行任何的文件操作时,系统当中默认就打开了以下的三个标准输入输出流 。
以上得到三个指针分别表示标准输入、标准输出、标准错误。
此时就要思考为什么默认就要把以上的三个文件流指针打开呢?
我们知道程序是用来做数据处理的,那么数据默认的获得和输出途径就应该是键盘和显示器,Linux 下一切皆文件 ,此时将这两个设备对应的文件流指针打开就是便于用户操作的。
3. 引入 I/O 系统调用 以上提到的 fopen 函数在打开对应的文件时打开文件时所带的选项决定了打开文件的方式,在之前 C 语言当中的学习知道有以下的打开方式。
在此我们最常见的就是使用读、写以及追加的方式进行打开。那么 fopen 函数内部究竟是如何实现不同的打开方式的呢?
以上通过 man 手册可以看出 open 系统调用是有两个的,一个是带 mode 参数另一个则没有。在此就存在问题了,那就是在 C 语言当中我们知道是不支持函数重载的,那么以上为了能同时存在两个函数名一样的函数呢?其实在这两个函数本质上是一个函数,这个函数是一个可变参数函数 。
那么 open 系统调用要如何使用呢,其实使用的方式和 C 语言当中的 fopen 是非常类似的,第一个参数都是要打开文件的文件名 ,之后的第二个参数是要打开文件的标志位 ,最后一个参数 mode 是当打开的文件不存在时创建对应文件的权限 。
通过 man 手册就可以看到标志位的传输依靠的是 6 个宏的:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
//这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使⽤
O_APPEND: 追加写成功:新打开的文件描述
O_TRUNC:创建文件时将原本文件内的内容覆盖
本质上标志位就是使用了位图 的思想,以上的 6 个宏就可以进行按位或的方式将标记位进行传递,这样就避免了需要使用过多的参数才能实现的问题。
为了更好的理解 open 系统调用当中的标志位参数是如何传递的,接下来来看以下的实例:
#include <stdio.h>
#define ONE (1)
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)
void flags (int flag) {
if (flag& ONE)printf ("flag have 1\n" );
if (flag& TWO)printf ("flag have 2\n" );
if (flag& THREE)printf ("flag have 3\n" );
if (flag& FOUR)printf ("flag have 4\n" );
printf ("-------------------------------\n" );
}
int main () {
flags (ONE);
flags (TWO);
flags (THREE);
flags (FOUR);
flags (ONE | TWO);
flags (ONE | TWO | THREE);
flags (ONE | TWO |THREE |FOUR);
return 0 ;
}
以上代码当中创建了 4 个宏,接下来使用不同的宏进行给 flags 函数,该函数内就可以得到函数实参当中传递过来的宏包括哪些。
open 内标志位实现其实就和以上代码实现的方式类似。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
int fd=open ("text.txt" ,O_WRONLY | O_CREAT | O_TRUNC);
if (fd<0 ) {
perror ("fopen error!" );
}
close (fd);
return 0 ;
}
以上代码实现的就是将 text.txt 文件在当前路径下以写的方式打开,并且创建 text.txt 文件清空创建。
那么在执行以上的程序时先将原来创建的 text.txt 删除,之后再执行以上程序看看有什么效果。
注:以上使用到了 close 关闭文件的系统调用,使用该系统调用的使用需要引用头文件#include<unistd.h>。
通过使用 ll 指令就可以看出此时创建的 text.txt 文件的权限是有问题的,其实这时因为在使用 open 的时候没有传对应的权限的参数 。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
int fd=open ("text.txt" ,O_WRONLY | O_CREAT | O_TRUNC,0666 );
if (fd<0 ) {
perror ("fopen error!" );
}
close (fd);
return 0 ;
}
进行以上的修改之后创建出来的 text.txt 文件的权限就正常了,但是这时还有一个问题就是为什么我们使用 open 系统调用的时候传入的是权限是 666,最终生成出来的文件的权限却是 664 呢?
其实该问题在之前学习权限相关的知识的时候就已经讲解过了,我们传的权限其实并不是最终生成的文件权限,最终文件的权限=~umask& 起始权限 ,在此起始权限就是我们传的权限,而 umask 就是权限掩码。在 Linux 当中默认的权限掩码是 2 。
那么如果我们就想在创建对应的 text.txt 文件的时候 umask 的值为 0,但是又想在其他的进程当中 umask 的值还是保持为 2,那么这时要如何操作呢?
以上使用 open 系统调用能实现打开对应的文件,那么如果要对文件进行写入就需要使用到write 系统调用。
通过以上 man 手册当中的说明就可以看出 write 的第一个参数是对应的要进行写入操作的文件调用 open 的返回值,第二个参数是要进行写入内容的指针,最后一个参数是要进行写入内容的字节数。
以下就是将 text.txt 文件使用 open 打开之后在使用 write 来进行文件的写入
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
int fd=open ("text.txt" ,O_WRONLY | O_CREAT | O_TRUNC,0666 );
if (fd<0 ) {
perror ("fopen error!" );
}
const char * msg="hello world\n" ;
int len=strlen (msg);
int cnt=5 ;
while (cnt--) {
write (fd,msg,len);
}
close (fd);
return 0 ;
}
以上就是使用 open 结合 write 系统调用来实现打开 text.txt 文件再向该文件当中写入对应的内容。以上的代码编译为对应的可执行程序之后执行之后 text.txt 文件当中就可以看到以下的内容。
以上代码当中调用 open 的打开时候方式的标志位是只读、覆盖式的创建,那么如果要实现打开文件之后追加方式写入,那么这时就需要将标志位修改为 O_WRONLY | O_APPEND 。
以上的代码修改为向 text.txt 文件当中实现追加代码如下所示:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
int fd=open ("text.txt" ,O_WRONLY | O_APPEND,0666 );
if (fd<0 ) {
perror ("fopen error!" );
}
const char * msg="hello world\n" ;
int len=strlen (msg);
int cnt=5 ;
while (cnt--) {
write (fd,msg,len);
}
close (fd);
return 0 ;
}
运行以上的代码查看 text.txt 文件内的内容就会发现进行了追加操作。
以上是向文件当中写入数据,那么如果要在指定的文本当中读取数据又要使用什么样的系统调用呢?
在此操作系统当中提供了 read 的系统调用来实现。
read 函数的第一个参数是打开文件返回的值,第二个参数是要从文件当中读取数据存放的临时内存的指针,最后一个参数是读取的数据的字节数。
该系统调用的返回值为读取文件得到的字节数,当读取失败的时候返回值为 -1。
以上示例当中对之前我们创建的 text.txt 函数来实现读取 :
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
int fd=open ("text.txt" ,O_RDONLY );
if (fd<0 ) {
perror ("fopen error!" );
}
const char * msg="hello world\n" ;
char buffer[1024 ];
while (1 ) {
ssize_t s= read (fd,buffer,strlen (msg));
if (s>0 )printf ("%s" ,buffer);
else break ;
}
close (fd);
return 0 ;
}
以上我们就了解了 Linux 当中进行文件操作的系统调用,那么了解了这些系统调用之后就可以理解 C 当中给我们实现的关于文件的操作是如何实现的,其实本质上C 当中使用的文件相关的函数就是封装了操作系统当中提供的系统调用。
例如 fopen 就是封装了 open 系统调用,而 fopen 能实现不同的方式打开文件其实是不同打开方式的 fopen 函数的封装的 open 的 flags 参数不同,在 fopen 当中当用户传入的打开方式参数是w 时封装的 open 当中 flags 参数实参就为O_WRONLY | O_CREAT | O_TRUNC ,当用户是以a 方式打开的时候,open 当中 flags 参数实参就为O_WRONLY | O_APPEND ,当用户以r 方式打开的时候 open 当中实参就为 O_RDONLY。
除了 fopen 之外,fclose 就是封装为了 open 实现的,而在 C 语言当中在对文件进行写入或者是读取的时候是有很多的方式的,有按照文本方式写入的,还有按照二进制进行写入的;其实这些函数本质上都是调用了 write 该系统调用,write 都是按照二进制的方式进行写入的,而那些不同的写入方式是语言层上提供的。
其实 C/C++等的语言在不同的操作系统当中其库函数封装底层的系统调用都是不一样的,但是表层提供给用户的函数接口 都是一样的,这些语言这样做的原因是为了提高其在不同平台的兼容性;这样就可以使得同一份的代码在不同的平台下都能可以正常的运行,这样可以使得用户群体更加的多样。语言通过封装系统调用来实现各种功能这就是运用了面向对象 的三大特点其中的封装 。
4. 文件描述符 以上我们使用到 open 系统调用的时候返回值 fd 是什么呢?要解答该问题就需要了解文件描述符的概念
我们知道操作系统当中是默认打开三个标准输入输出流的,而通过之前的学习知道使用 fopen 的时候其返回值本质上是结构体指针。在该结构体当中存在一个名为 fileno 的变量其实该变量就是文件描述符。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
printf ("stdin->%d\n" ,stdin->_fileno);
printf ("stdout->%d\n" ,stdout->_fileno);
printf ("stderr->%d\n" ,stderr->_fileno);
return 0 ;
}
以上就通过访问三个标准输入、输出流当中的 fileno 变量,看看有什么特点。
通过输出就可以看出标准输入对应的文件描述符是 0,标准输出默认的文件描述符是 1,标准错误默认的文件描述符是 2。
那么是不是就是说明操作系统当中文件描述符是从 0 开始的呢?
确实是这样的,有了这么长时间的编程经历看到以 0 为开始的结构我们马上就能想到数组,其实以上提到的文件描述符本质上就是数组的下标 。
其实当对应的进程打开文件之后在操作系统内存当中就会为每个文件产生相应的文件 file 对象 ,该结构体内存储了文件的相关内容以及属性 。和之前我们学习进程的管理类似操作系统对文件的管理也是先描述再组织 ,那么创建 file 结构体的对象就是进行先描述 ,那么之后再进行再组织这些结构体对象其实是通过数组来实现的;在操作系统当中对于每个进程都会有一个文件描述符表 ,其本质就是一个指针数组,每个数组的元素就是一个指向 file 的指针 。
以上提到的文件描述符表的指针存储对应进程的task_struct 当中。当在进程当中打开其他的文件时会从下标 3 开始将对应的文件的指针填充在数组上,这样就可以将新打开的文件也进行先描述再组织。
5. 重定向 先在当前路径下创建 text.txt1、text.txt2、text.txt3 文件
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
printf ("stdin->%d\n" ,stdin->_fileno);
printf ("stdout->%d\n" ,stdout->_fileno);
printf ("stderr->%d\n" ,stderr->_fileno);
int fd1=open ("text.txt1" ,O_RDONLY );
int fd2=open ("text.txt2" , O_RDONLY);
int fd3=open ("text.txt3" ,O_RDONLY );
printf ("fd1->%d\n" ,fd1);
printf ("fd2->%d\n" ,fd2);
printf ("fd3->%d\n" ,fd3);
printf ("fd1->%d" ,fd1);
printf ("fd2->%d" ,fd2);
printf ("fd3->%d" ,fd3);
close (fd1);
close (fd2);
close (fd3);
return 0 ;
}
以上我们依次打开 text.txt1,text.txt2,text.txt3 那么就会依次将这几个文件的 file 对象指针填到文件描述表当中。
那么这是就有一个点值得思考了,那就是如果在以上的代码当中分别将标准输入输入、标准输出和标准错误关闭会行打开的文件的文件描述符又会有什么特点呢?
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
close (0 );
printf ("stdin->%d\n" ,stdin->_fileno);
printf ("stdout->%d\n" ,stdout->_fileno);
printf ("stderr->%d\n" ,stderr->_fileno);
int fd1=open ("text.txt1" ,O_RDONLY );
int fd2=open ("text.txt2" , O_RDONLY);
int fd3=open ("text.txt3" ,O_RDONLY );
printf ("fd1->%d\n" ,fd1);
printf ("fd2->%d\n" ,fd2);
printf ("fd3->%d\n" ,fd3);
printf ("fd1->%d" ,fd1);
printf ("fd2->%d" ,fd2);
printf ("fd3->%d" ,fd3);
close (fd1);
close (fd2);
close (fd3);
return 0 ;
}
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
close (1 );
printf ("stdin->%d\n" ,stdin->_fileno);
printf ("stdout->%d\n" ,stdout->_fileno);
printf ("stderr->%d\n" ,stderr->_fileno);
int fd1=open ("text.txt1" ,O_WRONLY );
int fd2=open ("text.txt2" , O_RDONLY);
int fd3=open ("text.txt3" ,O_RDONLY );
printf ("fd1->%d\n" ,fd1);
printf ("fd2->%d\n" ,fd2);
printf ("fd3->%d\n" ,fd3);
return 0 ;
}
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
close (2 );
printf ("stdin->%d\n" ,stdin->_fileno);
printf ("stdout->%d\n" ,stdout->_fileno);
printf ("stderr->%d\n" ,stderr->_fileno);
int fd1=open ("text.txt1" ,O_WRONLY );
int fd2=open ("text.txt2" , O_RDONLY);
int fd3=open ("text.txt3" ,O_RDONLY );
printf ("fd1->%d\n" ,fd1);
printf ("fd2->%d\n" ,fd2);
printf ("fd3->%d\n" ,fd3);
return 0 ;
}
注:以上在打开文件的之后未将文件使用 close 关闭是因为关闭文件之后出现的现象就需要使用到缓冲区的概念来解释,但是当前缓冲区的概念我们还未了解,因此在此就不使用 close。
通过以上的三段代码就可以看出当出现小位置的文件描述符未被使用的时候,新打开的文件分配的文件描述符就会从小的开始分配。因此打开新的文件时分配的文件描述符的原则是:分配未被使用的且是最小的 。
了解了以上的概念之后,在此就可以引入重定向的原理 了,其实重定向就是改变原本进程当中的文件描述符表内数组下标对应的元素。
以上的代码 1 当中就是将原本文件描述符表当中的 0 下标内的指针修改为 text.txt1 文件 file 指针。
以上的代码 1 当中就是将原本文件描述符表当中的 1 下标内的指针修改为 text.txt1 文件 file 指针。
以上的代码 1 当中就是将原本文件描述符表当中的 2 下标内的指针修改为 text.txt1 文件 file 指针。
以上我们通过先关闭对应文件的方式确实是能实现重定向的,但是其实一般是不会使用这种方式的,而是使用系统调用dup2
通过 man 手册当中就可以看到,dup 系统调用实现的是将文件描述符表当中oldfd 下标的内的指针拷贝到newfd 下标当中。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
int fd1=open ("text.txt1" ,O_WRONLY );
int fd2=open ("text.txt2" , O_RDONLY);
int fd3=open ("text.txt3" ,O_RDONLY );
dup2 (fd1,1 );
printf ("fd1->%d\n" ,fd1);
printf ("fd2->%d\n" ,fd2);
printf ("fd3->%d\n" ,fd3);
return 0 ;
}
以上的代码就先打开三个文件之后再将文件描述符为 1 的数组下标内的指针修改为 text.txt1 文件的 file 对象指针,此时程序原本要输出到显示器上的内容就会输出到 text.txt1 文件当中。
那么有了 dup2 系统调用,就可以使用 dup2 要将标准输出对应的文件描述符的下标内容替换指定的 file 指针
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
int fd1=open ("text.txt1" ,O_WRONLY );
dup2 (fd1,1 );
printf ("fd1->%d\n" ,fd1);
return 0 ;
}
以上的代码当中就将新打开大的 text.txt1 对应的文件 file 指针将文件描述符表当中 1 下标的内容覆盖。
在之前的学习当中我们已经了解过了输出重定向,输入重定向和追加重定向是如何使用的,那么结合本篇当中以上的学习就有一个疑惑了,那就是默认的标准输出和标准错误都是向显示器当中输出,那么这时为什么还要有标准错误呢?不是直接使用一个标准输出就可以实现了吗?
其实在一般的情况下标准输出和标准错误输出的都是显示器文件,但是可以使用重定向 来将常规的输出消息和错误的消息进行分离,就例如计算机或者是服务器当中错误都是写入到日志当中的,其实现的原理就是将原本要输出到显示器上儿童通过重定向来输出到指定的文件当中。
#include <iostream>
#include <stdio.h>
int main () {
std::cout<<"hello cout" <<std::endl;
printf ("hello printf\n" );
std::cerr<<"hello cerr" <<std::endl;
fprintf (stderr,"hello stderr\n" );
return 0 ;
}
以上的代码当中就分别使用 C 和 C++ 当中的方法向标准输出和标准错误当中输出了数据,将以上代码编译为可执行程序之后输出的内容如下所示:
如果要将以下程序输出的结果输入到指定的文件当中我们知道可以使用到>来实现。
以上我们将 stream 执行的内容输出到了 text.txt 文件当中,但是为什么以上还是会在显示器当中就输出到标准错误当中的内容显示到显示器当中呢?
要解答以上的问题就需要了解输出重定向的本质,实际上在使用 > 输出重定向时,其实本质上执行的是 可执行程序 1 > 指定的文件 。以上的命令本质上执行的是 ./stream 1 > text.txt 也就是将原本要要输出到标准输出文件当中的内容输出到指定的文件当中,但是这时原来我们的代码当中还有两句代码是要输出到标准错误当中的,标准错误对应的文件描述符是 2;因此执行以上的指令还是会将标准错误的内容输出到显示器上。
那么如果要将标准错误的内容也输出到 text.txt 当中,那么这时要怎么操作呢?
在此有两种方式可以解决;第一种是使用追加重定向的方式就标准错误的内容追加到文件当中
第二种是直接将文件描述符表当中 1 下标当中的内容覆盖到下标为 2 当中
6. 给 myShell 添加重定向功能 通过以上的学习我们已经了解了重定向实现的原来,那么接下来就试着来给之前实现的 myshell 添加上重定向 的功能,让用户可以在命令行当中将程序执行的结果输出到指向的文件当中。
要给给 myshell 当中添加重定向的功能,那么就需要在获取到用户输入的内容之后再进行是否要进行重定向的分析,以下就再RedirCheck 函数当中使用对应的功能。
当用户再命令行当中输入重定向的指令之后其实是可以将用户的指令分为以下的两个部分的
分别是重定向符之前的内容以及重定向符之后的内容,那么此时就需要创建一个字符串来存储指令后半部分的内容。
那么如何让原本输出到标准输入或者从标准输入当中读取替换为使用重定向符之后的文件当中进行读取或者输出呢?
要实现以上的功能就需要在原本执行程序的函数当中在使用 fork 创建子进之前打开对应的文件之后再使用 dup2 来实现重定向。
#define NONE_ENDIR 0
#define INPUT_REDIR 1
#define OUTPUT_DEDIR 2
#define APPEND_REDIR 3
int redir=NONE_ENDIR;
std::string filename;
接下来就可以试着来对用户读取的指令当中判断是否要进行重定向的操作
void TrimSpace (char * cmd,int &end) {
while (isspace (cmd[end])) {
end++;
}
}
void RedirCheck (char * cmd) {
redir=NONE_ENDIR;
filename.clear ();
int start=0 ;
int end=strlen (cmd)-1 ;
while (start<end) {
if (cmd[end]=='<' ) {
cmd[end++]=0 ;
TrimSpace (cmd,end);
redir=INPUT_REDIR;
filename=cmd+end;
} else if (cmd[end]=='>' ) {
if (cmd[end-1 ]=='>' ) {
cmd[end-1 ]=0 ;
redir=APPEND_REDIR;
}
else {
redir=OUTPUT_DEDIR;
}
cmd[end++]=0 ;
TrimSpace (cmd,end);
filename=cmd+end;
} else {
end--;
}
}
}
注:以上使用到 open 就需要在代码当中添加上头文件 <sys/stat.h>和<fcntl.h>
以上在实现了重定向的分析之后接下来就需要在执行原来的命令当中在子子进程当中实现重定向的功能。
int Execute () {
pid_t pid=fork();
if (pid==0 ) {
int fd=-1 ;
if (redir==INPUT_REDIR) {
fd=open (filename.c_str (),O_RDONLY);
if (fd<0 )exit (1 );
dup2 (fd,0 );
close (fd);
} else if (redir==OUTPUT_DEDIR) {
fd=open (filename.c_str (),O_CREAT | O_WRONLY |O_TRUNC,0666 );
if (fd<0 )exit (1 );
dup2 (fd,1 );
close (fd);
} else if (redir==APPEND_REDIR) {
fd=open (filename.c_str (),O_CREAT | O_WRONLY | O_APPEND,0666 );
if (fd<0 )exit (1 );
dup2 (fd,1 );
close (fd);
}
execvp (g_argv[0 ],g_argv);
exit (1 );
}
int status=0 ;
pid_t rid=waitpid (pid,&status,0 );
if (rid>0 ) {
lastcode=WEXITSTATUS (status);
}
return 0 ;
}
在实现了以上的代码之后就有一个问题需要思考了,那就是在进行进程替换之前进行的重定向会不会因为进程替换的而影响呢?
答案是不会的,通过之前的学习我们知道进程替换的本质实际上是没有创建新的进程而是在物理内存当中将原来进程的代码和数据替换为指定进程的代码和数据。
在 main 函数当中可以在调用完 RedirCheck 函数之后将 redir 和 filename 的值打印出来看看是否符合要求。
int main () {
InitEnv ();
while (true ) {
PrintCommandPrompt ();
char commandline[COMMAND_SIZE];
if (!GetCommandLine (commandline,sizeof (commandline))) continue ;
RedirCheck (commandline);
printf ("redir:%d,filename:%s\n" ,redir,filename.c_str ());
if (!CommandParse (commandline)) continue ;
if (CheckAndExecBuiltin ()) continue ;
Execute ();
}
return 0 ;
}
将 myshell.cc 重新编译之后生成 myshell,之后执行 myshell 看看是否能实现重定向的功能。
通过以上的输出结果就能说明实现的重定向功能是符合要求的。
完整代码 #include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <cstring>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
int lastcode=0 ;
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "
#define MAXARGC 128
char * g_argv[MAXARGC];
int g_argc=0 ;
#define MAX_ENVS 200
char * g_env[MAX_ENVS];
int g_envs=0 ;
std::string prev_pwd;
#define NONE_ENDIR 0
#define INPUT_REDIR 1
#define OUTPUT_DEDIR 2
#define APPEND_REDIR 3
int redir=NONE_ENDIR;
std::string filename;
const char * GetUserName () {
const char * name=getenv ("USER" );
return name==NULL ?"None" :name;
}
const char * GetHostName () {
const char * hostname=getenv ("HOSTNAME" );
return hostname==NULL ?"None" :hostname;
}
const char * GetPwd () {
static char * cur_pwd=nullptr ;
if (cur_pwd!=NULL ) {
free (cur_pwd);
}
cur_pwd=getcwd (NULL ,0 );
return cur_pwd==NULL ? "None" :cur_pwd;
}
const char * GetHome () {
const char * home=getenv ("HOME" );
return home==NULL ?"" :home;
}
void InitEnv () {
extern char ** environ;
memset (g_env,0 ,sizeof (g_env));
g_envs=0 ;
for (int i=0 ;environ[i];i++) {
g_env[i]=(char *)malloc (strlen (environ[i])+1 );
strcpy (g_env[i],environ[i]);
g_envs++;
}
g_env[g_envs]=NULL ;
for (int i=0 ;g_env[i];i++) {
putenv (g_env[i]);
}
environ=g_env;
}
bool cd () {
if (!(g_argc==2 && (strcmp (g_argv[1 ],"-" )==0 ))) {
prev_pwd=GetPwd ();
}
if (g_argc==1 ) {
std::string home=GetHome ();
if (home.empty ())return true ;
chdir (home.c_str ());
} else {
std::string where=g_argv[1 ];
if (where=="-" ) {
std::string tmp=GetPwd ();
std::cout<<prev_pwd<<std::endl;
chdir (prev_pwd.c_str ());
prev_pwd=tmp;
}
else if (where=="~" ) {
std::string home=GetHome ();
chdir (home.c_str ());
}
else {
chdir (where.c_str ());
}
}
int pwd_idx = -1 ;
for (int i = 0 ; g_env[i] != NULL ; i++) {
if (strncmp (g_env[i], "PWD=" , 4 ) == 0 ) {
pwd_idx = i;
break ;
}
}
char * cwd = getcwd (NULL , 0 );
if (cwd == NULL ) {
return "None" ;
}
size_t len = strlen (cwd) + 5 ;
char * pwd_str =(char *) malloc (len);
if (pwd_str == NULL ) {
free (cwd);
return "None" ;
}
snprintf (pwd_str, len, "PWD=%s" , cwd);
free (cwd);
if (pwd_idx != -1 ) {
free (g_env[pwd_idx]);
g_env[pwd_idx] = pwd_str;
} else {
if (g_envs < MAX_ENVS - 1 ) {
g_env[g_envs++] = pwd_str;
g_env[g_envs] = NULL ;
}
}
return true ;
}
void Echo () {
if (g_argc==2 ) {
std::string opt=g_argv[1 ];
if (opt=="$?" ) {
std::cout<<lastcode<<std::endl;
lastcode=0 ;
} else if (opt[0 ]=='$' ) {
std::string env_name=opt.substr (1 );
const char *env_vlue=getenv (env_name.c_str ());
if (env_vlue) std::cout<<env_vlue<<std::endl;
} else {
std::cout<<opt<<std::endl;
}
}
}
bool Export () {
char * newenv =(char *)malloc (strlen (g_argv[1 ])+1 );
strcpy (newenv,g_argv[1 ]);
g_env[g_envs++]=newenv;
g_env[g_envs]=NULL ;
return true ;
}
std::string DirName (const char * pwd) {
#define SLASH "/"
std::string dir=pwd;
if (dir==SLASH)return SLASH;
auto pos=dir.rfind (SLASH);
if (pos==std::string::npos)return "BUG" ;
return dir.substr (pos+1 );
}
void MakeCommandLine ( char cmd_prompt[],int size ) {
snprintf (cmd_prompt,size,FORMAT,GetUserName (),GetHostName (),DirName (GetPwd ()).c_str ());
}
void PrintCommandPrompt () {
char prompt[COMMAND_SIZE];
MakeCommandLine (prompt,sizeof (prompt));
printf ("%s" ,prompt);
fflush (stdout);
}
bool GetCommandLine (char * out,int size) {
char * c=fgets (out,size,stdin);
if (c==NULL )return false ;
out[strlen (out)-1 ]=0 ;
if (strlen (out)==0 )return false ;
return true ;
}
bool CommandParse (char *commandline) {
#define SEP " "
g_argc=0 ;
g_argv[g_argc++]=strtok (commandline,SEP);
while ((bool )(g_argv[g_argc++]=strtok (nullptr ,SEP)));
g_argc--;
return g_argc>0 ? true :false ;
}
void Print () {
for (int i=0 ;g_argv[i];i++) {
printf ("argv[%d]->%s\n" ,i,g_argv[i]);
}
printf ("argv:%d\n" ,g_argc);
for (int i=0 ;i<g_envs;i++) {
printf ("env[%d]->%s\n" ,i,g_env[i]);
}
}
bool CheckAndExecBuiltin () {
std::string cmd=g_argv[0 ];
if (cmd=="cd" ) {
cd ();
return true ;
} else if (cmd=="echo" ) {
Echo ();
return true ;
} else if (cmd=="export" ) {
Export ();
return true ;
} else {
}
return false ;
}
void TrimSpace (char * cmd,int &end) {
while (isspace (cmd[end])) {
end++;
}
}
void RedirCheck (char * cmd) {
redir=NONE_ENDIR;
filename.clear ();
int start=0 ;
int end=strlen (cmd)-1 ;
while (start<end) {
if (cmd[end]=='<' ) {
cmd[end++]=0 ;
TrimSpace (cmd,end);
redir=INPUT_REDIR;
filename=cmd+end;
} else if (cmd[end]=='>' ) {
if (cmd[end-1 ]=='>' ) {
cmd[end-1 ]=0 ;
redir=APPEND_REDIR;
}
else {
redir=OUTPUT_DEDIR;
}
cmd[end++]=0 ;
TrimSpace (cmd,end);
filename=cmd+end;
} else {
end--;
}
}
}
int Execute () {
pid_t pid=fork();
if (pid==0 ) {
int fd=-1 ;
if (redir==INPUT_REDIR) {
fd=open (filename.c_str (),O_RDONLY);
if (fd<0 )exit (1 );
dup2 (fd,0 );
close (fd);
} else if (redir==OUTPUT_DEDIR) {
fd=open (filename.c_str (),O_CREAT | O_WRONLY |O_TRUNC,0666 );
if (fd<0 )exit (1 );
dup2 (fd,1 );
close (fd);
} else if (redir==APPEND_REDIR) {
fd=open (filename.c_str (),O_CREAT | O_WRONLY | O_APPEND,0666 );
if (fd<0 )exit (1 );
dup2 (fd,1 );
close (fd);
}
execvp (g_argv[0 ],g_argv);
exit (1 );
}
int status=0 ;
pid_t rid=waitpid (pid,&status,0 );
if (rid>0 ) {
lastcode=WEXITSTATUS (status);
}
return 0 ;
}
int main () {
InitEnv ();
while (true ) {
PrintCommandPrompt ();
char commandline[COMMAND_SIZE];
if (!GetCommandLine (commandline,sizeof (commandline))) continue ;
RedirCheck (commandline);
if (!CommandParse (commandline)) continue ;
if (CheckAndExecBuiltin ()) continue ;
Execute ();
}
return 0 ;
}
7. 理解'一切皆为文件' 在之前 Linux 的学习当中我们就一直听到一句话就是 Liinux 下一切皆文件,但是之前只是知道了这就话;而没有真正的理解这就话的原理,那么接下来就从本质上理解什么是一切皆文件。
在解释以上的问题之前先来了解文件的缓冲区是在什么位置的,其实在文件的 file 结构体当中存在一个指向一块内存的指针,该指针指向的就是文件的缓冲区,除此之外还会有一个指向另外一个结构体的指针,该结构体内存储着文件的属性。
其实在计算机的硬件当中不同的硬件进行读写的操作方式都是不同的,但是不同的硬件都是需要有读写的能力的,那么在操作系统当中就在文件的 file 结构体当中添加了一个硬件读写的函数指针,这样就就可以使得访问设备都是通过函数指针来访问之后,该函数的类型和参数都是相同的。
在以上当中其实将各个文件的 file 连接在一起整个结构被称为VFS(虚拟文件系统) ,这时就可以将该结构视为基类,而以下的各个硬件就视为派生类,整个结构就实现出了C 语言版的多态 。
上图中的外设,每个设备都可以有自己的 read、write,但⼀定是对应着不同的操作方法!!但通过 struct file 下 file_operation 中的各种函数回调,让我们开发者只⽤ file 便可调取 Linux 系统中绝⼤部分的资源!!这便是'linux 下⼀切皆⽂件'的核心理解。
在 Linux 当中使用以上的结构设计的这样做最明显的好处是,开发者仅需要使用⼀套 API 和开发工具,即可调取 Linux 系统中绝大部分的资源。
8. 缓冲区 其实在之前的学习当中我们就已经在许多的地方涉及到了缓冲区,例如在 f 了解 flush、了解 exit 和_exit 的区别的时候,但是在之前我们只是知道有缓冲区这一概念而不知道缓冲区具体是在那里。接下来来就来详细的了解缓冲区的概念。
其实在操作系统当中是存在两个不同的缓冲区的,分别是文件内核缓冲区 和语言层缓冲区 。
我们知道当使用 fopen 打开一个文件的时候返回值是一个 FILE 类型的指针,那么这时在操作系统当中就会创建出一个 FILE 的结构体对象,在该结构体当中存储在文件描述符等的数据,还会有一个指向缓冲区的指针。
实际上在语言层面要对文件进行写入操作的时候是会将数据先存储在语言的缓存区上,之后当满足要求之后再将缓冲区内的数据拷贝到文件内核大的缓冲区当中。
那么问题就来了,若要将语言层缓冲区的数据刷新到文件内核的缓冲区当中需要满足什么条件呢?
以上满足以下三个条件其中之一即可实现刷新
用户执行强制刷新
刷新条件满足
程序退出之前
以上的强制刷新和程序退出之前刷新比较容易理解,但是这个刷新条件满足又是什么呢?
其实在语言的层面对应不同的硬件实现了不同的刷新策略,一般来说会有三种的刷新策略,分别是立即刷新、全刷新、按照行刷新。立即刷新一般是用于无缓冲区的情况,全刷新则是当缓冲区满了再刷新这种刷新的效率是最高的,行刷新则一般是用在显示器当中。
以上就了解了缓冲区的基本概念,那么接下来就要思考缓冲区存在的意义是什么呢?并且为什么要有这么多的刷新策略呢?为什么不能当语言层的缓冲区当中一有数据就直接刷新呢?
其实缓冲区实现出来的最终目的是为了提高使用者的效率 ,在将语言层缓冲区内的数据刷新到文件内核缓冲区当中实际上是通过调用系统调用来实现的,我们知道调用任何的系统调用都是要消耗系统系统的资源的,那么如果无论什么都使用立即刷新的方式就会使得效率受到影响,因此对于不同的文件类型需要有不同的刷新策略。
当将语言层当中的数据刷新到文件内核缓冲区时,就将数据交给了 OS,那么这时就可以认为将数据传给了硬件。
接下来就通过两段代码来进一步的理解缓冲区是如何在实际当中运作的
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
close (1 );
int fd1=open ("text.txt1" ,O_CREAT | O_WRONLY | O_APPEND,0666 );
printf ("fd1->%d\n" ,fd1);
printf ("hello world\n" );
printf ("hello world\n" );
printf ("hello world\n" );
const char * msg="hello\n" ;
write (fd1,msg,strlen (msg));
return 0 ;
}
以上的代码当中先将标准输出关闭,之后以追加的方式打开文件 text.txt1,那么这时该文件文件描述符就为 1,之后代码当中原本要写入到显示器当中的数据都会输出到该文件当中。之后使用 printf 和 write 进行输出,那么这时我们就可以预测到该代码执行之后会将以上输出语句内数据都写入到 text.txt1 文件当中。
那么如果这时在以上的代码最后使用 close 将该文件关闭,执行的结果还会和原来一样吗?
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
close (1 );
int fd1=open ("text.txt1" ,O_CREAT | O_WRONLY | O_APPEND,0666 );
printf ("fd1->%d\n" ,fd1);
printf ("hello world\n" );
printf ("hello world\n" );
printf ("hello world\n" );
const char * msg="hello\n" ;
write (fd1,msg,strlen (msg));
close (fd1);
return 0 ;
}
执行之后会发现文件当中只有 hello 这一条一句被写入了,这是为什么呢?
在了解了缓冲区的知识之后以上的问题就很容易解答了,当在关闭文件之前使用 printf 写入的数据还是存储在语言层的缓冲区当中的,这时将文件关闭之后是不满足三个刷新条件其中之一的,那么这时就只会将使用 write 系统调用写入到文件内核缓冲区当中数据存储到文件当中。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main () {
close (1 );
int fd1=open ("text.txt1" ,O_CREAT | O_WRONLY | O_APPEND,0666 );
printf ("hello printf\n" );
fprintf (stdout,"hello fprintf\n" );
const char * msg="hello write\n" ;
write (fd1,msg,strlen (msg));
fork();
return 0 ;
}
以上的代码当中当不添加 fork 的时候很好理解就是向对应的文件当中输出三条语句。
当时当添加上 fork 之后竟然会先 text.txt1 当中输出以下的内容。
要解答以上的问题还是需要结合缓冲区的知识,以上在 fork 创建子进程之前使用 C 语言函数输出的两条语句其实还是在语言层的缓冲区当中 ,因为子进程在创建的时候会将父进程的代码和数据全部拷贝,这就会使得在子进程语言层缓冲区也会有这两条语句。而使用 write 系统调用的十直接写入到文件内核缓冲区当中,该操作是在 fork 之前的子进程就不会进行这一操作。因此最终就会向 text.txt1 当中写入两次 hello printf 和 hello sprintf
当你能完全理解以上两段代码的原理就说明你已经理解缓冲区的原理了
9. 简单设计 libc 库 以上在了解了缓冲区的相关概念之后就可以试着来实现自己的 libc 库,最终实现之后在程序当中能通过调用库中的函数实现文件的读写等操作,类似 C 当中提供的 fopen、fwrite、fclose。
首先创建一个全新的目录,在该目录下创建三个文件分别为 mystdio.h、mystdio.c、user.c,再创建对应的 makefile 文件,makefile 要实现的是将 mystdio.c 和 user.c 编译生成可执行程序 mystdio。
mystdio:mystdio.c user.c
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f mystdio
接下来在 mysydio.h 内实现各个函数的定义
#pragma once
#include <stdio.h>
#define MAX 1024
#define NONE_FLUSH (1<<0)
#define LINE_FLUSH (1<<1)
#define PULL_FLUSH (1<<2)
typedef struct IO_FILE {
int fileno;
int flag;
char outbuffer[MAX];
int bufferlen;
int flush_method;
}Myfile;
Myfile* MyFopen (const char * path,const char * mode) ;
void MyFclose (Myfile* ) ;
size_t MyFwrite (Myfile*,void *str,int len) ;
void MyFFlush (Myfile*) ;
以上实现了函数的声明之后就可以在 mystdio.c 内实现函数的定义
#include "mystdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
static Myfile* BuyMyfile (int flag,int fd) {
Myfile* f=(Myfile*)malloc (sizeof (Myfile));
if (f==NULL )return NULL ;
f->fileno=fd;
f->flag=flag;
f->bufferlen=0 ;
f->flush_method=LINE_FLUSH;
memset (f->outbuffer,0 ,sizeof (f->outbuffer));
return f;
}
Myfile* MyFopen (const char * path,const char * mode) {
int fd=-1 ;
int flag=0 ;
if (strcmp (mode,"w" )==0 ) {
flag=O_WRONLY |O_CREAT |O_TRUNC;
fd=open (path,flag,0666 );
}
else if (strcmp (mode,"r" )==0 ) {
flag=O_RDONLY;
fd=open (path,flag);
}
else if (strcmp (mode,"a" )==0 ) {
flag=O_WRONLY | O_CREAT | O_APPEND;
fd=open (path,flag,0666 );
}
else {
}
return BuyMyfile (flag,fd);
}
void MyFclose (Myfile* file) {
if (file->fileno<0 )return ;
MyFFlush (file);
close (file->fileno);
free (file);
}
size_t MyFwrite (Myfile* file,void *str,int len) {
memcpy (file->outbuffer+file->bufferlen,str,len);
file->bufferlen+=len;
if ((file->flush_method & LINE_FLUSH) && file->outbuffer[(file->bufferlen)-1 ]=='\n' ){
MyFFlush (file);
}
return len;
}
void MyFFlush (Myfile* file) {
if (file->bufferlen<0 )return ;
int n=write (file->fileno,file->outbuffer,file->bufferlen);
(void )n;
fsync (file->fileno);
file->bufferlen=0 ;
}
以上就实现了 mystdio.c 内的各个函数,以上在 MyFFlush 当中使用到了 fsync 系统调用,该系统调用的作用是**将文件在内存中的修改强制同步到磁盘,**这样就可以实现每次在将缓冲区的数据刷新的时候能同步到对应的文件当中。
接下来在 user.c 当中添加 stdio.h 的头文件之后就可以调用我们实现的函数来实现文件的操作。
#include "mystdio.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main () {
Myfile* f=MyFopen ("./text.txt" ,"a" );
if (f==NULL ) {
exit (1 );
}
int cnt=5 ;
char * str=(char *)"hello world\n" ;
while (cnt--) {
MyFwrite (f,str,strlen (str));
printf ("buffer:%s\n" ,f->outbuffer);
sleep (1 );
}
MyFclose (f);
return 0 ;
}
以上代码当中在打开 text.txt 文件之后向文件当中写入,在写入过程当中每秒观察一次缓冲区的情况。
以上就会发现当以上代码是在程序结束的时候一次性的刷新。那么如果要写入一次就刷新一次就需要在写入之后进行刷新
#include "mystdio.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main () {
Myfile* f=MyFopen ("./text.txt" ,"a" );
if (f==NULL ) {
exit (1 );
}
int cnt=5 ;
char * str=(char *)"hello world\n" ;
while (cnt--) {
MyFwrite (f,str,strlen (str));
MyFFlush (f);
printf ("buffer:%s\n" ,f->outbuffer);
sleep (1 );
}
MyFclose (f);
return 0 ;
}