文章目录
- 一、 基础文件控制
- 1.1 系统接口open函数
- 1.2 Linux中文件描述符
- 1.2 C语言FILE中的文件描述符
- 二、重定向
- 1. 输出重定向
- 2. 追加重定向
- 3. 输入重定向
- tips:fd的分配规则
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
一、 基础文件控制
- 以下为C语言中默认打开的三个文件流(C++中为cin、cout、cerr)
值得一提的是:以上三个默认打开的文件分别为键盘文件、显示器文件、显示器文件,在任一进程中使用系统接口(如open/read/write时)时依次对应(文件描述符fd)下标0、1、2
头文件<unistd.h>定义了常量 STDIN_FILENO、STDOUT_FILENO和 STDERR_FILENO,它们可用来代替显式的描述符值。
- 对应功能:0标准输入, 1标准输出, 2标准错误
标准输出流和标准错误流的区别利用管道可以更好解释,不在此文解释
1.1 系统接口open函数
函数返回值是新打开文件的文件描述符。
函数第一个参数pathname,表示要打开或创建的目标文件。
- 若pathname以绝对路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
- 若pathname只以文件名的方式给出,则当需要创建该文件时,默认在当前工作路径下进行创建。
函数的第二个参数是flags,表示打开文件的方式。
其中常用选项有如下几个:
参数选项 | 含义 |
---|---|
O_RDONLY | 以只读的方式打开文件 |
O_WRNOLY | 以只写的方式打开文件 |
O_APPEND | 以追加的方式打开文件 |
O_RDWR | 以读写的方式打开文件 |
O_CREAT | 当目标文件不存在时,创建文件 |
O_TRUNC | 打开文件的时候会将文件原本的内容全部丢弃,文件大小变为 0 |
打开文件时,可以传入多个参数选项,用“或”运算符隔开。
若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置为O_WRONLY | O_CREAT
值得一提的是,flags的类型为int
,而这些参数的本质也确实是。
宏定义选项的共同点就是,它们的二进制序列当中有且只有一个比特位是1(O_RDONLY选项的二进制序列为全0,表示O_RDONLY选项为默认选项),且为1的比特位是各不相同的,这样一来,在open函数内部就可以通过使用**“与”运算**来判断是否设置了某一选项。
函数的第三个参数是mode,表示创建文件的默认权限。
将mode设置为0666,则理论上文件创建出来的权限如下:
但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。
若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。
umask(0); //将文件默认掩码设置为0
注意: 当不需要创建文件时,open的第三个参数可以不必设置
1.2 Linux中文件描述符
文件描述符fd(file descriptor)就是内核为了高效管理这些已经被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符来实现。
Linux内核中task_struct 结构体作为进程的抽象封装,是 Linux 里面最复杂的结构体之一 ,成员字段非常多,不仅包括描述虚拟空间的mm_struct,还有描述管理文件的file_struct结构体,这个结构体为管理某进程打开的所有文件的管理结构
下列为file_struct代码与部分task_struct代码
struct task_struct {//此处只列出与本文相关部分// ...struct mm_struct *mm;//struct mm_struct *active_mm;/* Open file information: */struct files_struct *files;// ...
}struct files_struct {// 读相关字段atomic_t count;bool resize_in_progress;wait_queue_head_t resize_wait;// 打开的文件管理结构struct fdtable __rcu *fdt;struct fdtable fdtab;// 写相关字段unsigned int next_fd;unsigned long close_on_exec_init[1];unsigned long open_fds_init[1];unsigned long full_fds_bits_init[1];struct file * fd_array[NR_OPEN_DEFAULT];
};
files_struct 本质上就是用数组管理的方式来管理所有打开的文件的。该进程下所有打开的文件结构都在数组里。数组在那里?有两个地方:
- struct file * fd_array[NR_OPEN_DEFAULT] 是一个静态数组,随着 files_struct 结构体分配出来的,在 64 位系统上,静态数组大小为 64;
- struct fdtable 可以理解成用于存储文件结构体的一个动态数组,数组边界是用字段描述的;
简化结构体如下:
struct fdtable {unsigned int max_fds;struct file __rcu **fd; /* current fd array */
};
其中 max_fds 指明数组边界。
fdtable.fd 指向的内存地址还是存储指针的(指针类型为 struct file * )。即 fdtable.fd (二级指针)指向一个数组,数组元素为指针(指针类型为 struct file *)
以上这种的动静态结合设计使得可以方便地动态分配和替换文件描述符数组,以满足需要扩展文件描述符数组的情况。
下图为了表示简便,未展示fdtable
综上,不难理解fd 即管理struct file*数组的索引,也就是数组的槽位编号而已。 通过非负数 fd 就能拿到对应的 struct file 结构体的地址。
而struct file结构体是用来标识进程打开的某一个文件的。简化结构如下:
struct file {// ...struct path f_path;struct inode *f_inode;const struct file_operations *f_op;atomic_long_t f_count;unsigned int f_flags;fmode_t f_mode;struct mutex f_pos_lock;loff_t f_pos;struct fown_struct f_owner;// ...
}
下面为 IO 相关的几个重要的字段:
- f_path :标识文件名
- f_inode :inode 这个是 vfs 的 inode 类型,是基于具体文件系统之上的抽象封装;
- f_pos : 当前文件偏移。f_pos 在 open 的时候会设置成默认值,seek 的时候可以更改,从而影响到 write/read 的位置;
注意:
- struct file 是属于系统级别的结构,是可以共享与多个不同的进程。即多个进程同时指向一个文件(如fork出的父子进程)。
- 在同一个进程中,多个 fd 可能指向同一个 file 结构,利用dup函数即可
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
1.2 C语言FILE中的文件描述符
语言的库函数都是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的,所以C库当中的FILE结构体内部必定封装了文件描述符fd。
首先,我们在/usr/include/stdio.h
头文件中可以看到下面这句代码,也就是说FILE实际上就是struct _IO_FILE结构体的一个别名。
typedef struct _IO_FILE FILE;
而我们在/usr/include/libio.h
头文件中可以找到struct _IO_FILE结构体的定义,在该结构体的众多成员当中,我们可以看到一个名为_fileno的成员,这个成员实际上就是封装的文件描述符。
所以C语言当中的其他文件操作函数,如fread、fwrite、fputs、fgets等函数都是在做什么?
fopen函数在上层为用户申请FILE结构体变量,并返回该结构体的地址(FILE*),在底层通过系统接口open打开对应的文件,得到文件描述符fd,把fd填充到FILE结构体当中的_fileno变量中,至此便完成了文件的打开操作。
之后这些操作函数都是根据我们传入的文件指针找到对应的FILE结构体,然后在FILE结构体当中找到文件描述符,最后通过文件描述符调用系统接口对文件进行的一系列操作。
二、重定向
重定向格式 :流 >/>> 文件
重定向有三种类型:
- 输出重定向:将本应该打印到 显示器 的内容覆盖式输出到了指定的文件中。
- 追加重定向:将本应该打印到 显示器 的内容追加式的输出到了指定的文件中
- 输入重定向:将本应该从 键盘中 读取的内容改为从指定的文件中读取。
重定向的本质是:修改文件描述符fd下标 对应的struct file * 的内容 (将其换成目标文件)。
1. 输出重定向
使用重定向操作符“>”,后面接文件名,就可以把标准输出重定向到另一个文件中,而不是显示在屏幕上。其实现的本质可以利用以下代码理解
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{close(1);int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open");return 1;}printf("hello world\n");fflush(stdout);close(fd);return 0;
}
fd为1的标准输出文件被关闭,紧接着打开的log.txt
文件fd即为1,运行后显示器上并没有输出数据,对应数据输出到了log.txt文件当中。
printf函数是默认向stdout输出数据的,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。
这与cat >log1.test
的作用其实一样,因为cat xx.txt > other.txt 其实这种写法其实是简写,把1省略了,完整写法cat xx.txt 1> other.txt。,即将显示器 的内容覆盖式输出到了指定的文件中
2. 追加重定向
重定向符号
>>
追加重定向与输出重定向的区别在于前者为追加式输出
原理测试方法与上式相同,但在open函数中需要将第二个参数添加O_APPEND以实现追加输入
int fd = open("log.txt", O_WRONLY|O_APPEND|O_CREAT, 0666);
3. 输入重定向
当命令执行后我们并不希望得到输出,而是想把这个输出丢弃,尤其是在输出错误和状态信息的情况下更为需要。系统提供了一种方法,即通过把输出重定向到一个称为/dev/null的特殊文件中来实现它。这个文件是一个称为位桶(bit bucket)的系统设备,它接受输入但是不对输入进行任何处理
>/dev/null : 这是一种简写,完整的写法是 1 >/dev/null 。 在linux中,默认的重定方向就是保证输出流,也就是1 。那么2>&1 又是什么意思呢? 也很简单,这也是重定向的结构。2表示标准错误流。& 表示等同的意思,也就是说跟1的情况一样,跟1采取相同方式,即标准错误流跟标准输出流采取同样会的处理方式,也就是重定向到空设备。这个后缀经常使用在linux命令中,表示不输出任何内容。
tips:fd的分配规则
当有新的文件被打开,需要分配fd时,在fd array[]中从小到大,按照顺序找最小的且没有被占用的fd,来进行分配;
具体验证可以采用系统接口close(1)关闭原来的显示器文件,新建文件后在向该文件cat 输入内容,会发现该内容也会在显示器上打印,即该文件此时的fd为1。
(注意scanf函数是默认从stdin读取数据的,而stdin指向的FILE结构体中存储的文件描述符是0,因此scanf实际上就是向文件描述符为0的文件读取数据。)
1 #include <stdio.h>2 #include <sys/types.h>3 #include <sys/stat.h>4 #include <fcntl.h>5 #include <unistd.h>6 7 int main()8 {9 10 close(1);1112 umask(0000);13 int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);//没有指明文件路径,默认在当前进程的工作目录14 if(fd<0)15 {16 perror("open");17 return 1;18 }19 20 printf("open fd:%d\n",fd);// printf --> stdout21 fprintf(stdout,"open fd:%d\n",fd);// fprintf --> stdout 22 23 fflush(stdout); 24 close(fd);25 return 0;26 }
实际上我们使用重定向时,重定向的是文件描述符是1的标准输出流,而并不会对文件描述符是2的标准错误流进行重定向。