
前言这篇介绍 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读取、写入、关闭与定位拿到文件描述符以后就可以围绕它操作文件了。接口主要参数作用与返回结果readfd、buf、count从fd对应的文件读取最多count个字节放进buf返回实际读取的字节数writefd、buf、count把buf中的内容写入fd对应的文件最多写入count个字节返回实际写入的字节数closefd关闭这个文件描述符成功返回 0失败返回-1lseekfd、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时掌握的访问方式也就能够用于更多类型的对象。完