文章

apue笔记

apue笔记

篇幅比较短的小节就直接列入章节下的有序列表中,节省整篇笔记的篇幅

unix标准及实现

POSIX标准定义的必须的头文件

POSIX标准定义的XSI可选头文件

ISO C标准定义的头文件

unix标准化

  • ISO C:国际标准化组织 C程序设计语⾔国际标准⼯作组,这个标准旨在提供C程序的可移植性,而不只适用于unix系统。
  • IEEE POSIX:电气和电子工程师学会 可移植操作系统接口,这个标准旨在提升应用在各种unix系统环境之间的可移植性
  • SUS:单一unix规范,是POSIX.1标准的一个超集,而POSIX.1相当于SUS的基本规范部分。POSIX.1中的XSI(X/Open系统接口)描述了可选的接口,也定义了遵循XSI的实现必须支持POSIX.1的哪些可选部分。只有遵循XSI的实现才能称为unix系统

限制

unix系统定义了很多常量用来限制程序运行时的一些行为,用来改善unix环境下的软件可移植性。以下两种类型的限制是必须的:

  1. 编译时限制(比如短整型的最大值?)
  2. 运行时限制(比如文件名最多几个字符?)

三种限制方式:

  1. 编译时限制(头文件)
  2. 与文件或目录无关的运行时限制(sysconf函数)
  3. 与文件或目录相关的运行时限制(pathconf和fpathconf函数)

功能测试宏:使用#define _POSIX_C_SOURCE 200809L,使得所有POSIX.1头文件都将使用此常量来排除任何专有的实现,提高可移植性。

基本系统数据类型:头文件<sys/types.h>定义了与实现有关的数据类型(比如clock_tfd_set),提高可移植性。

文件I/O

本章描述的函数均为unbuffered I/O。

  1. 对于内核而言所有打开的文件都通过文件描述符引用,文件描述符是一个非负整数。按照惯例unix的shell会把0与标准输入关联,1与标准输出关联,2与标准错误关联
  2. open / openat:打开/创建文件,第四个参数只有创建文件时才需要用到,指定创建文件的modeopenat多了一个fd参数可以指定相对路径
  3. 创建文件时,如果文件名过长可能会被截断并忽略 或者 返回错误,具体行为通过使用pathconf查询_POSIX_NO_TRUNC决定。
  4. creat:等效于open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)
  5. close:释放fd,并且释放加在文件上的所有记录锁。就算不主动调用close,进程终止时内核也会自动关闭进程打开的所有文件
  6. lseek:每个打开的文件都有一个”当前偏移量“来度量从文件开始处计算的字节数,一般是非负整数(某些设备也可能允许负的偏移量)。lseek用来显示设置该偏移量。如果文件是管道、FIFO、网络套接字等”流“文件,lseek会返回-1且errno设为ESPIPE若偏移量大于当前文件长度,下一次写文件时在文件中构成一个空洞,并用\0填充。这个空洞不要求占用磁盘空间,具体方式与文件系统的实现有关。
  7. read:返回已经读到字节数。达到文件末尾则返回0。对于流设备(如网络套接字),如果fd不是非阻塞的话(可以通过fcntl改变这一行为),那么对于没有数据接收并且连接未关闭连接时,read将阻塞。
  8. write:返回值通常与nbytes参数的值相同,否则表示出错。出错的一个常见原因是磁盘已经写满,或者超过文件长度限制
  9. BUFFSIZE对拷贝文件效率的影响:假设磁盘块长度为4096。当BUFFSIZE超过32,时钟时间下降趋势趋于稳定;当BUFFSIZE超过4096,系统CPU时间下降趋势趋于稳定。前者原因在于BUFFSIZE超过32时,系统调用次数不再成为主要性能瓶颈,而在于I/O;后者原因在于BUFFSIZE超过4096时,由于物理页与虚拟页大小仍然是4096,因此主要开销变成了内存页缓存管理和I/O。
  10. sync(void) / fsync(fd) / fdatasync(fd):对于write只是将数据写入内核缓冲区,当内核需要重用这块缓冲区时才会将数据写入磁盘。sync将修改过的块缓冲区排入写队列,但不等待写磁盘结束,名为update的系统守护进程也会周期性调用sync定期冲洗内存缓冲区;fsync将数据和文件元信息马上冲刷到磁盘;fdatasync只冲刷数据。
  11. fcntl(fd,cmd,...):拥有五种功能:复制描述符(可以用dup/dup2实现)、读写fd标志(目前只有FD_CLOEXEC一种标志,所以现在很多程序都不用这个常量,而是直接用1表示close_on_exec,0则取消)、读写文件状态标志、读写异步I/O所有权、读写记录锁
  12. ioctl(fd,request,...):I/O操作的杂物箱,不能用其它函数表示的I/O操作通常用ioctl来操作。比如磁带的倒带、越过制定个数的记录等。
  13. /dev/fd/n:打开/dev/fd文件夹里面命名为数字n的文件相当于复制文件描述符n,即dup(n)。但是在linux中的/dev/fd/n则是指向其它文件的符号链接,打开这些文件的时候,和一般的open没有区别。这些文件主要用于让只接受文件路径参数的程序也能处理文件描述符、在shell脚本中实现一些高级的I/O骚操作。

文件共享

unix支持在不同进程间共享打开文件。为此介绍一下内核使用3种数据结构来表示打开的文件。

  1. 每个进程维护一个打开文件描述符表,每个表项包含文件描述符标志、指向一个文件表项的指针。
  2. 内核为所有打开文件维护一张文件表,每个文件表项包含文件状态标志(读、写、同步、非阻塞等)、当前偏移量、指向v节点表项的指针。
  3. 每个打开文件都有一个vnode结构,vnode包含了文件类型、对该文件进行各种操作函数的指针、inode(包含所有者、文件长度、在磁盘上所在位置的指针等)。操作系统使用vnode的目的是支持多文件系统类型,称为虚拟文件系统VFS。

一个进程打开了标准输入和标准输出

两个独立进程打开了同一个文件

原子操作

如图3.8所示,若两个进程同时要往文件末尾写入新内容,那么需要先lseek到文件末尾再write,那么两个进程的其中一个写入的内容就有可能被覆盖。因此,我们希望lseekwrite作为一个原子操作,有两种解决方法:

  • 如果只需原子地追加内容,可以通过在打开文件时指定O_APPEND标志,该标志会使得lseek设置偏移量只影响read,而write一定会将内容写入文件末尾。
  • 如果需原子地往文件中间读/写,可以通过pread/pwrite实现,但注意pread/pwrite不会更新偏移量。

另外,还有一种「打开文件不存在则创建」的原子操作,解决方法是指定O_EXCL | O_CREAT

dupdup2函数

dup(fd)/dup2(fd, fd2)用于复制一个现有的文件描述符。其中dup返回的一定是当前可用的最小文件描述符,而dup2可以用fd2指定新的描述符的值,如果fd2已经打开则先将其关闭(如果fd=fd2则不用做此操作)。

注意dupdup2返回的fd都会清除FD_CLOEXEC这个文件描述符标志,这样,fd在调用exec时不会被关闭。

举个例子,假设此时执行dup(1)并且下一个可用的fd为3:

执行dup(1)且下一个可用的fd为3

dup/dup2也可以用fcntl实现:

  • dup(fd)等效于fcntl(fd,F_DUPFD,0),第三个参数表示新fd为大于等于0的下一个可用fd
  • dup2(fd,fd2)等效于close(fd2); fcntl(fd,F_DUPFD,fd2)

习题

  • 3.5:结合打开文件的数据结构(见“文件共享”小节)以及「shell是从左到右处理命令行」,只需要模拟fd文件指针的指向变化即可。一开始1和2的文件指针分别指向stdout和stderr。对于./a.out > outfile 2>&1,首先把1指向了outfile的文件表项,然后把2指向1所指向的文件表项,最终两者都指向outfile的文件表项。而对于./a.out 2>&1 > outfile,先把2指向1指向的文件表项即stdout,再把1指向outfile的文件表项,最终2指向stdout,1指向outfile。
  • 3.6:对于使用O_APPEND打开的文件,lseek只影响read,而对write无效,无论如何都会写入文件末尾。这保证了追加写入的原子性(见“原子操作”小节)

文件和目录

  1. stat/fstat/lstat/fstatatstat以文件名方式获取文件信息,fstatfd方式获取,lstat以文件名方式获取符号链接文件的信息(即不follow链接指向的原文件),fstatat功能最强大,可以覆盖以上三种函数的用法。

  2. 与进程对文件操作相关联的ID:「实际用户/组ID」(登录时就确定下来,标识用户实际上是谁),「有效用户/组ID、附属组ID」(用于文件访问权限检查),「保存的设置用户/组ID」(由exec函数保存)。通常有效用户/组ID等于实际用户/组ID。执行文件的时候可以通过设置st_mode将执行权限临时设置为文件所有者,从而可能拥有更高的执行权限,这个时候「保存的设置用户/组ID」就用来保存原来st_mode中的相应位。

  3. st_mode还保存了文件的9个访问权限位(所有者、组、其它)。

  4. 访问某个路径下的文件,都应该具有这个路径上每个目录的执行权限。如果只有目录的读权限,那只能ls列出文件夹里文件的文件名等信息,而不能访问这些文件。

  5. 删除文件所需的权限:只需要对包含该文件的目录具有写、执行权限,对文件本身不需要读写权限。

  6. 新创建的文件的用户/组ID:用户ID为有效用户ID,组ID可以设置为有效组ID或者是所在目录的组ID。

  7. 文件的SUID位/SGID位:通过chmod u+s file设置文件的SUID位,使得该文件执行时使用的是文件所有者ID,即进程的有效用户不再是实际用户,而是文件所有者。在程序中可以使用access/faccessat检查实际用户是否对文件有相应的权限

  8. umask文件模式创建屏蔽字:创建文件时,尽管将mode设置为0777,创建完发现只有0755,原因是系统的文件模式创建屏蔽字umask默认为022,即最终的权限位为0777 & ~umask,可以通过umask(mode_t)函数更改默认的umask

  9. 粘着位S_ISVTX(通过chmod设置):当目录设置了粘着位,其中的内容只能被所有者重命名或删除。常见的比如/tmp/var/tmp文件夹设置了粘着位,因为/tmp是整个操作系统的程序都共用的,给/tmp设置粘着位可以避免删除或者重命名/tmp里面别人的文件。在权限位最后一位中显示为t(若没有执行权限则为大写T

  10. chown:若_POSIX_CHOWN_RESTRICTED生效,则不能更改文件的用户ID,不能将组更改为不属于文件的用户的组,只能由超级用户来更改。

  11. st_blksize / st_blocks:分别是文件I/O较为适合的长度(比如BUFFSIZE可以指定为st_blksize)以及文件实际上占用的512B的块数(有的操作系统可能不是512B)

  12. truncate / ftruncate:截断文件到指定长度,若长度大于文件本身长度,则用0填充文件。(可能是空洞也可能是实际字节)

  13. link / linkat / unlink / unlinkat:创建/删除对文件的链接(目录项)。一般的实现中都不能跨越文件系统创建链接、不能创建对目录的链接。当指向一个文件的目录项全部被删除了,直到没有进程打开该文件,其数据项才能从磁盘上删除。

  14. rename / renameat:重命名文件/目录,关于重命名的附加规则查看原书。

  15. ftw / nftw:用于简单遍历文件树。最新代码应该使用fts系列函数来实现相关功能。

  16. symlink / symlinkat / readlink / readlinkat:创建/读取符号链接。

  17. futimens / utimensat / utimes:更改文件的访问和修改时间,单位为纳秒或微秒。

  18. mkdir / mkdirat / rmdir:创建/删除文件夹,其中删除只能删除空文件夹。

  19. opendir / fopendir / readdir / closedir / telldir / seekdir:文件夹一般不能用read来读取,需要dirent.h来支持文件夹的相关读取操作。

  20. chdir / fchdir / getcwd:更改/获取当前工作目录

  21. st_dev, st_rdev:前者是文件系统上的设备号(主要用于文件系统级别的操作),后者是实际设备的设备号(只有字符和块设备才有这个属性,主要用于系统工具或设备驱动程序)。可以用major/minor这两个宏分别获取主、次设备号。对于st_dev来说,在同一个磁盘驱动器上的各个文件系统通常有相同主设备号,但次设备号不同。

文件类型

  1. 普通文件
  2. 目录文件,包含了其他文件的名字以及指向这些文件信息的指针
  3. 块特殊文件,提供对设备带缓冲的访问,每次访问以固定长度进行
  4. 字符特殊文件,提供对设备不带缓冲的访问,每次访问长度可变。系统中所有设备要么是块特殊文件要么是字符特殊文件
  5. FIFO(命名管道),用于进程间通信
  6. socket:用于网络通信
  7. 符号链接

可以通过宏S_ISxxx(st.st_mode)来确定是哪种文件

POSIX.1允许将IPC对象抽象成文件,比如信号量、消息队列等,可以通过宏S_TYPEISxxx(&st)来确定是哪种IPC对象

文件系统

磁盘、分区和文件系统

磁盘分为多个分区,每个分区可以包含一个文件系统。

i节点、数据块和目录块

一个i节点,多个目录项:从图中看出,多个目录项可以指向同一个i节点,每个i节点中都有一个链接计数,其值是指向i节点的目录项数。只有删除了所有这些目录项,这个i节点所代表的文件才真正能从磁盘上被删除。(并且打开这个文件的进程数为0)。

文件名:i节点保存了文件大多数的属性(文件类型,访问权限位,文件长度,数据块指针等),但是文件名和i节点编号保存在目录项中,这样就可以通过创建链接来创建文件名不同但实际上是同一个的文件。

i节点不能跨越文件系统:i节点编号只能唯一指向同一个文件系统中相应的i节点,因此不能目录项不能指向其它文件系统的i节点,也就是链接不能跨越文件系统。

文件重命名/移动:在同一文件系统中,使用mv对文件重命名/移动时,文件的实际内容并未移动,只需构造一个指向现有i节点的新目录项,并删除旧目录项。

目录文件的链接计数:至少为2,一个来自该目录文件本身的目录项,一个来该目录其中的.目录项。若该目录内包含其它目录,这个子目录的..目录项也会指向该目录文件的i节点,因此该目录内每增加一个子目录,链接计数就+1。

目录文件的链接计数

标准I/O库

维度文件描述符(FD)流(Stream)
本质整数,代表已打开文件FILE * 指针,封装了 FD
级别操作系统内核级C 标准库用户态
API 示例open, read, write, closefopen, fread, fwrite, fclose
缓冲无缓冲或手动控制内部有缓冲
适用范围低级文件、socket、管道等主要用于文件 I/O
灵活性可以 dup2 复制重定向不能直接用于 dup2

除了linux其他很多系统都实现了标准I/O库,因此这个库由ISO C标准进行说明。标准I/O为用户处理了很多细节,比如缓冲区分配、以优化的块长度执行I/O等。

  1. fwide(FILE*, int):设置流的导向(orientation),负数为字节导向,适合使用fgetc/fputc/fread/fwrite等函数处理ascii或者二进制数据;正数为宽导向,适合使用fgetwc/fputwc/fgetws/fputws等函数处理多字节字符集,如中文、日文。
  2. 预设标准输入输出stdin / stdout / stderr

缓冲

标准I/O提供3种类型的缓冲:

  1. 全缓冲:主动flush、缓冲区满了自动flush
  2. 行缓冲:主动flush、缓冲区满了自动flush、遇到换行符自动flush
  3. 无缓冲:字面意思

许多操作系统对于流的缓冲类型默认实现为如下:标准错误无缓冲,终端流为行缓冲,其它为全缓冲

其它参考

GNU 是什么,和 Linux 是什么关系? - 知乎

https://github.com/MeiK2333/apue

本文由作者按照 CC BY 4.0 进行授权