前言:这篇介绍 Linux 中常用的系统文件 IO 接口,理解文件描述符的作用,再看看重定向是怎么改变输入输出位置的,本篇和以前我写的【C语言】文件操作详细解析-CSDN博客练习紧密所以忘了的话可以看看。
1.从文件到系统接口
平时提到文件,我们首先想到的往往是里面的内容,比如一段文字、一份代码。但对于操作系统来说,管理一个文件还需要知道它的大小、权限、所属用户等信息。
所以可以先把一个文件理解成两部分:文件内容和文件属性。
即使文件大小是 0,也只是没有保存内容,文件的属性仍然需要记录。我们查看文件时看到的权限、大小、修改时间等,就是这些属性的一部分。
普通文件通常保存在磁盘上,程序退出以后,文件还可以继续保留。而磁盘属于外部设备,程序从文件中读取内容、向文件中写入内容,就是在进行 IO 操作。
文件由操作系统统一管理。程序想要访问文件,需要通过操作系统提供的接口,把“打开哪个文件”“读多少内容”“往哪里写”等要求交给它。
前面使用的 C 文件接口,也是在底层系统接口的基础上提供更方便的操作方式。这里直接来看 Linux 中的open、read、write、close等接口。
2.常用的系统文件 IO 接口
2.1open:打开文件
操作文件通常从打开文件开始。理解open,主要看三个参数:
| 参数 | 作用 |
|---|---|
pathname | 要打开的文件路径,可以是相对路径,也可以是绝对路径 |
flags | 以什么方式打开,是否需要创建、清空或者追加 |
mode | 创建新文件时使用的权限,使用O_CREAT时必须提供 |
打开成功后,open返回一个非负整数,叫作文件描述符;失败则返回-1。后面读写和关闭文件,都要用到这个描述符。
先看flags中最基本的三种访问方式:
| 访问方式 | 含义 |
|---|---|
O_RDONLY | 只读 |
O_WRONLY | 只写 |
O_RDWR | 可读可写 |
这三种方式选择一种即可。在此基础上,还可以按需要搭配其他选项:
| 选项 | 作用 |
|---|---|
O_CREAT | 文件不存在时创建文件 |
O_TRUNC | 以可写方式打开已有普通文件时,把原来的内容清空 |
O_APPEND | 每次写入时,都从文件末尾追加 |
例如,想要“只写,文件不存在就创建,存在就清空”,就把O_WRONLY、O_CREAT、O_TRUNC通过按位或运算符|组合起来。
这些名字实际上都是宏,可以看看它们的定义:
不过,访问方式需要单独记住:**O_RDONLY的值为 0,不能把它与O_WRONLY组合成读写方式,读写要直接选O_RDWR注意一下就行了。
再看创建文件时的权限。
只要使用了O_CREAT,就要传入mode,即使这次打开的文件已经存在,也不能省略。如果文件已经存在,这个参数不会重新修改它的权限。
创建新文件时,还要考虑umask。它会屏蔽掉一部分权限,常见情况下可以按mode & ~umask理解。例如给定0666,当前umask为0022,最终得到的权限就是0644,也就是rw-r--r--。
来看下面的图:
使用了O_CREAT,却没有给出创建权限,因此出现了不符合预期的权限结果。
除了上面那些还要区分两个概念:flags中的访问方式决定这次打开后准备怎么读写,mode决定新建文件申请哪些权限。
2.2读取、写入、关闭与定位
拿到文件描述符以后,就可以围绕它操作文件了。
| 接口 | 主要参数 | 作用与返回结果 |
|---|---|---|
read | fd、buf、count | 从fd对应的文件读取最多count个字节,放进buf,返回实际读取的字节数 |
write | fd、buf、count | 把buf中的内容写入fd对应的文件,最多写入count个字节,返回实际写入的字节数 |
close | fd | 关闭这个文件描述符,成功返回 0,失败返回-1 |
lseek | fd、offset、whence | 调整文件偏移量,成功返回调整后相对文件开头的位置,失败返回-1 |
这里的buf就是程序中用来存放数据的一块内存。读取时把文件内容放进来,写入时把里面的数据交出去。
对于普通文件,在请求读取的字节数大于 0 时,read返回 0,表示已经读到文件末尾;返回-1,表示读取失败。write失败也返回-1。
之前在学C语言的文件读写操作时实际上也是对这些底层的系统调用做了封装,经过封装后的C标准库就可以支持各个平台系统,底层上就是在不同的平台都相应的实现了一份对应的代码,再通过条件编译的方式。这样我们使用C标准库提供的文件读写接口就具有可移植性。
3.文件描述符与重定向
3.1文件描述符为什么是整数
前面知道,open返回的文件描述符是一个非负整数。程序拿着这个整数,就能继续读写文件,操作系统是怎么根据它找到文件的?
可以先看下面的简化关系图:
每个进程都有自己的文件描述符表。图中可以把它看作一个数组,数组的每个位置保存着一个指向内核文件对象的指针,文件描述符就是这个位置的下标。
沿着关系看,就是:
进程 → 文件描述符表 → 对应表项 → 内核中的打开文件对象。
内核中的打开文件对象会记录这次打开的读写方式、当前偏移量等信息,并关联到文件及其属性。
因此,当程序把fd交给read时,操作系统就能通过当前进程的描述符表找到对应对象,再进行读取。文件描述符只在当前进程的这张表里表示一个位置,不是某个磁盘文件永久不变的编号。
正常在终端中启动的程序,通常已经有三个标准描述符:
| 描述符 | 名称 | 通常的用途 |
|---|---|---|
| 0 | 标准输入 | 接收输入 |
| 1 | 标准输出 | 输出正常结果 |
| 2 | 标准错误 | 输出错误信息 |
它们在终端中运行时通常连接到终端,所以我们能通过键盘输入,在屏幕上看到输出。但0、1、2 并没有固定绑定键盘和屏幕,它们也可以指向普通文件等其他对象。
当 0、1、2 已经被占用,3 又是空闲的时,再打开一个文件,就会拿到描述符 3。这正好对应图中myfile的位置。
这里的分配规则是:使用当前最小的可用描述符编号。
3.2改变描述符指向的文件
如果把 0 号描述符关闭,再打开一个文件,会发生什么?
此时 0 是最小的空闲编号,新文件就会使用这个位置:
原来 0 号位置的连接被断开,新的myfile接到了这个位置上。程序之后仍然使用描述符 0,但操作对象已经变了。
如果这里以只读方式打开myfile,那么之后从 0 号描述符读取,就会读到myfile中的内容,这就是理解输入重定向的一个直观例子。
把同样的思路用在 1 号描述符上,就能理解输出重定向。
比如让 1 号描述符关联到一个普通文件,程序继续向标准输出写内容,内容就会进入这个文件。程序使用的描述符没有变,描述符关联的对象变了。
3.3使用 dup2 完成重定向
关闭一个描述符,再利用最小编号分配规则重新打开文件,可以帮助我们理解原理。实际安排指定的描述符时,使用dup2更直接。
它的两个主要参数是:
| 参数 | 含义 |
|---|---|
oldfd | 已经打开的描述符,我们想使用它所关联的文件对象 |
newfd | 要调整的目标描述符 |
dup2会让newfd指向oldfd所指向的打开文件对象。如果newfd原来已经打开,它会在完成这个操作时关闭原有的关联。
例如,先以可写方式打开输出文件,把得到的描述符作为oldfd,把 1 作为newfd,就能让标准输出指向这个文件。如果要安排标准输入,则先以允许读取的方式打开文件,再把目标描述符设为 0。
这里要看清参数的方向:先写已有的描述符,再写想要改变的描述符。
操作完成后,两个描述符引用的是同一个打开文件对象,共用当前文件偏移量等状态。dup2复制的是描述符的关联关系,文件内容并不会因此复制一份。
如果打开文件时得到的是 3,随后把它复制到 1,那么在不再需要额外的 3 号描述符时,可以关闭 3。1 号描述符仍然保持着与这个文件的连接,可以继续用于输出。
4.小总结
前面已经看到,同样的 0、1、2,可以关联终端,也可以关联普通文件。程序通过描述符和统一的读写接口,就能访问不同的对象。
这就是理解 Linux“一切皆文件”的一个角度:把不同对象组织成可以通过文件接口访问的形式,让程序使用相近的操作方式。
这样,上层程序使用统一的接口,下层再根据对象的不同执行各自的操作。我们学习read、write时掌握的访问方式,也就能够用于更多类型的对象。
完