目录
前言:
我们先来简单回顾一下C语言中的文件相关知识
● 打开文件的方式
r 打开文本文件进行读取。流位于文件的开头。r+ 用于读写。流位于文件的开头。w 将文件截断为零长度或创建用于写入的文本文件。流位于文件的开头。w+ 开放用于读写。
a 打开以附加(在文件末尾写入)。如果不存在,则创建该文件。流位于文件末尾。a+ 可读取和附加(在文件末尾写入)。如果不存在,则创建文件。读取的初始文件位置在文件的开头,但输出总是附加到文件的末尾。
● stdin & stdout & stderr
C语言 默认会打开三个输入输出流,分别是 stdin, stdout, stderr这三个流的类型都是 FILE*, fopen 返回值类型,文件指针
一、open函数
1.介绍
open man open
#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);pathname: 要打开或创建的目标文件flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行 “ 或 ” 运算,构成 flags 。mode:权限掩码,对创建的文件进行权限管理。参数 :O_RDONLY : 只读打开O_WRONLY: 只写打开O_RDWR : 读,写打开这三个常量,必须指定一个且只能指定一个O_CREAT : 若文件不存在,则创建它。需要使用 mode 选项,来指明新文件的访问权限O_APPEND: 追加写返回值:成功:新打开的文件描述符失败:-1
open
函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要
open
创建,则第三个参数表示创建文件
的默认权限
,
否则使用两个参数的
open
。
2.open函数返回值
在认识返回值之前,先来认识一下两个概念
:
系统调用 和 库函数
fopen fclose fread fwrite 都是 C 标准库当中的函数,我们称之为库函数( lib )。而, open close read write 都属于系统提供的接口,称之为系统调用接口回忆一下我们讲操作系统概念时,画的一张图
系统调用接口和库函数的关系,就如上图所示,所以我们可以认为文件系列的库函数都是对系统调用的封装,方便二次开发,实现代码的跨平台性
open的返回值通常叫做
文件描述符fd,文件描述符就是一个整数
文件描述符 0 & 1 & 2
● Linux 进程默认情况下会有 3 个打开的文件描述符,分别是标准输入 0 , 标准输出 1 , 标准错误 2.● 0,1,2 对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
char buf[1024];
ssize_t s = read(0, buf, sizeof(buf));
if(s > 0)
{
buf[s] = 0;
write(1, buf, strlen(buf));
write(2, buf, strlen(buf));
}
return 0;
}
那么文件描述符是用来做什么的呢,为什么open函数返回的是文件描述符呢?
我面先看下面图:
根据上面图片,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
那么对于其他系统调用接口read,write等大家直接man学习即可。
二、重定向
1.文件描述符的分配规则,重定向的本质
根据上图,在进程中打开一个文件就会在依次在fd_array指针数组中进行添加创建,我们看下面代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
输出结果:fd:3。
关闭
0
或者
2,再看:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
//close(2);
int fd = open("myfile", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
发现结果是:
fd: 0
或者
fd 2
可见,文件描述符的分配规则:在
files_struct
数组当中,找到当前没有被使用的
最小的一个下标,作为新的文件描述符。
2.重定向的本质
那如果关闭
1
呢?看代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件
myfile
当中,其中,
fd
=
1
。这种现象叫做输出重定向。常见的重定向有
:>, >>, <
那重定向的本质是什么呢?
![](https://i-blog.csdnimg.cn/direct/0f35daede9b143408d77b1efcbcc569d.png)
重定向的本质是在指针数组中将对应下标位置的的指针所指向的内容进行修改,使其指向另一个文件,那么再向原先的文件下标中进行写入操作,操作就会打印到所指向的另一个文件中。
3.dup2系统调用
函数原型:
#include <unistd.h>int dup2(int oldfd, int newfd);
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd = open("log", O_CREAT | O_RDWR,0666);
if (fd < 0)
{
perror("open");
return 1;
}
close(1);
dup2(fd, 1);//将下标为1的数组内容更改成fd所指的内容,是1指向fd所指的文件
printf("hello Linux!\n");
return 0;
}
输出结果为创建一个log文件,里面包含“hello Linux!”字符串。
printf
是
C
库当中的
IO
函数,一般往
stdout
中输出,但是
stdout
底层访问文件的时候,找的还是
fd:1,
但此时,
fd:1
下标所表示内容,已经变成了log文件
的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写
入,进而完成输出重定向。
三、C语言库函数中的缓冲区
● 因为 IO 相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过 fd 访问的。● 所以 C 库当中的 FILE 结构体内部,必定封装了 fd。
来段代码在研究一下:
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
运行出结果:
hello printf
hello fwrite
hello write
但如果对进程实现输出重定向呢?
./hello > file
, 我们发现结果变成了:
hello write
hello printf
hello fwrite
hello printf
hello fwrite
我们发现
printf
和
fwrite
(库函数)都输出了
2
次,而
write
只输出了一次(系统调用)。为什么?大概是和fork有关
● 一般 C 库函数写入文件时是全缓冲的,而写入显示器是行缓冲。● printf fwrite 库函数会自带缓冲区,当发生重定向到普通文件时,数据 的缓冲方式由行缓冲变成了全缓冲。● 而我们放在缓冲区中的数据,就不会被立即刷新,甚至是 fork 之后● 但是进程退出之后,会统一刷新,写入文件当中。● 但是 fork 的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的 一份数据,随即产生两份数据。● write 没有变化,说明没有所谓的缓冲区。
综上:
printf fwrite
库函数会自带缓冲区,而
write
系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,
OS
也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢?
printf fwrite
是库函数,
write
是系统调用,库函数在系统调用的
“
上层
”
,是对系统
调用的
“
封装
”
,但是
write
没有缓冲区,而
printf fwrite
有,足以说明,该缓冲区是二次加上的,又因为是
C
语言,所以由
C
标准库提供。