APUE(一) IO与文件操作
文件IO
不带缓冲的io(unix系统在内核中设有缓冲区,这个不带缓冲意思是用户不自己缓冲),每个read和write都调用内核中的一个系统调用。
文件描述符
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。
Unix系统shell把文件描述符0与进程的标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准错误关联。在符合POSIX.1的程序中,幻数0,1,2已被标准化为符号常量STDIN_FILENO, STDOUT_FILENO和STDERR_FILENO。这些常量定义在<unistd.h>文件中。文件描述符的变化范围是0~OPEN_MAX-1。
文件IO函数
1 |
|
oflag参数可以使用下列一个或多个常量进行“或”运算构成oflag参数,此参数用来说明此函数的多个选项。
O_RDONLY只读打开 0O_WRONLY只写打开 1O_RDWR读写打开 2O_EXEC只执行打开O_SEARCH只搜索打开(–)
上述5个常量中必须指定一个且只能指定一个。O_APPEND写时追加,每次写时追加到文件末尾O_CLOEXEC把FD_CLOEXEC常量设置为文件描述符的标志O_CREAT若此文件不存在则创建它O_DIRECTORY如果path引用的不是目录,则出错O_EXCL如果同时指定了O_CREAT,而文件已经存在,则出错,可以用此测试文件是否存在O_NOCTTY如果path引用的时终端设备,则不将该设备分配作为此进程的控制终端O_NOFOLLOW如果path引用的时一个符号链接,则出错O_NONBLOCK如果path引用的是一个FIFO,块特殊文件或字符文件,则设置本次打开及后续操作为非阻塞i/o操作O_SYNC每次write都等待物理i/o操作完成O_TRUNC如果文件存在,而且为只写或读-写成功打开,则将其长度截断为0O_TTY_INIT如果打开一个还未打开的终端设备,设置非标准termios的参数值,使其符合Single Unix SpecificationO_DSYNC使每次write都要等待物理i/o操作完成O_RSYNC使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作完成
用open和openat函数返回的文件描述符一定是最小的未用描述符数值。
fd参数把open和openat参数区分开,共有三种可能性
- path时绝对路径名,这种情况忽略fd参数,openat相当于openat
- path参数指定的时相对路径名,fd参数指出相对路劲名的开始地址,fd参数是通过打开相对路径名所在的目录来获取的
- path参数制定了相对路径名,fd参数具有特殊值AT_FDCWD,这种情况下,路径名在当前工作目录中获取,openat函数在操作上与open函数一致
openat()是在新增的一类函数之一希望解决两个问题:
- 让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录
- 可以避免time-of-check-to-time-of-use(TOCTTOU)错误
creat(const char *path, mode_t mode)函数等效于open(path, O_WRONLY|O_CREAT|O_TRUNC, mode)。creat函数以只写的方式打开创建的文件。
关闭一个文件时还会释放该进程加在该文件上的所有记录锁。当一个进程终止时,内核会自动关闭它所有的打开文件。
- 每个打开文件都有一个与其相关的当前文件偏移量(current file offset),用以度量从文件开始处计算的字节数。
- 通常读,写操作都是从当前文件偏移量开始,并使偏移量增加所读写的字节数
- 系统默认的情况,当打开一个文件时,除非指定
O_APPEND选项,否则偏移量设为0
可以使用lseek函数为打开的文件设置偏移量。成功返回新的文件偏移量,出错返回-1。该方法也可以用来判断涉及的文件是否可以设置偏移量。
1 |
|
参数offset与参数whence的值有关:
- 若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。
- 若whence是SEEK_CUR,则将该文件的偏移量设置为其当前值加上offset,offset可为正或负。
- 若whence是SEEK_END,则将该文件的偏移位置为文件长度加offset,offset可为正或负。
lseek仅当当前文件偏移量记录都在内核中,并不引起I/O操作,然后偏移量被用于下一个读写操作
文件偏移量可以大于文件长度,对该文件的下一次写将加长该文件并形成一个空洞,位于文件中但是没有写过的字节都被读为0
文件空洞不要求在磁盘上占用存储区,具体处理方式和文件系统有关,因为lseek使用的偏移量是用offset表示的,所以允许具体实现根据各自特定平台自行选择大小合适的数据类型。
od -c命令可以观察文件中的实际内容,-c标志以字符方式打印文件内容。
1 |
|
有多种情况可以使读到的字节数少于要求读的字节数:
- 读普通文件时,读到文件要求的字节数前已经到达文件尾端
- 当从终端设备读时,通常一次最多读一行
- 当从网络读时,网络中的缓冲区机制可能造成返回值小于要求读的字节数
- 当从管道和FIFO读时,如果管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数
- 当一信号中断,而已经读了的部分数据量时
读操作从文件的当前偏移量处开始,在成功返回之前,该偏移量将增加时机读到的字节数。
返回值通常和nbytes的值相同,否则意味着出错,一次成功写,offset增加实际写的字节数。
write出错的常见原因是磁盘已经满了,或者超过了一个给定进程的文件长度限制。
对于普通文件,写操作从文件的当前偏移量开始,如果在打开该文件时指定了O_APPEND选项,则每次都将文件偏移量移到末尾。
大多数文件系统为了改善性能采取了预读技术,预读就是当检测到正在顺序读的时候,系统就试图读取比应用所要求的更多的数据。
如果重复度量程序性能,可能后续得到的结果比第一次好,因为后续各次运行一般从系统高速缓存访问,不需要在从文件系统进入高速缓存。
文件共享操作
unix系统支持在不同进程间共享打开的文件。内核用于I/O的数据结构。
内核使用3种数据结构表示打开文件:
- 每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,每个文件描述符表中项都包含:
- 文件描述符标志。
- 指向一个文件表项的指针。
- 内核为所有打开文件维持一张文件表项,每个文件表包含:
- 文件状态标志(读,写,添加,同步和非阻塞等)。
- 当前文件偏移量。
- 指向该文件v结点 表项的指针。
- 每个打开文件(或设备)都有一个v结点结构(linux下的i-node)。v结点包含了文件类型和对此文件进行各种操作的函数的指针。

两个独立进程各自打开同一个文件,每个进程有自己的文件表项,即拥有自己的当前偏移量。lseek函数只修改文件表项中的当前文件偏移量,不进行任何IO操作。

原子操作
一般而言原子操作(atomic operation)指的是由多步组成的一个操作。如果该操作原子的执行,则要么执行完所有的步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
1 | ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset) ; |
复制文件描述符操作:
1 |
|
dup2是一个原子操作,而close和fcntl包含两个函数调用。
传统的UNIX系统实现在内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候写入磁盘。这种方式被称为延迟写(delayed write)。
update系统守护进程周期性的调用sync函数,定期(大约30s)flush缓冲区。
1 |
|
fcntl可以改变已经打开文件的属性。
1 |
|
fcntl函数有一下5种功能:
- 复制一个已有的描述符.(cmd=F_DUPFD/F_DUPFD_CLOEXEC)
- 获取/设置文件描述符标志.(cmd=F_GETFD/F_SETFD)
- 获取/设置文件状态标志.(cmd=F_GETFL/F_SETFL)
- 获取/设置异步I/O所有权.(cmd=F_GETOWN/F_SETOWN)
- 获取/设置记录锁.(cmd=F_GETLK,F_SETLK,F_SETLKW)
- F_DUPFD 复制文件描述符fd
- F_DUPFD_CLOEXEC 复制文件描述符,设置与新文件描述符关联的FD_CLOEXEC文件描述符值,返回新文件描述符
- F_GETFD 对于fd的文件描述符标志作为函数值返回
- F_SETFD 对于fd设置文件描述符标志
- F_GETFD 对应于fd的文件状态标志作为函数值返回。
- F_SETFL 将文件状态标志设置为第三个参数的值
- F_GETOWN 获取当前SIGIO和SIGURG信号的进程ID或进程组id。
fcntl的返回值与命令有关,如果出错都返回-1,成功返回某个其他值。
在修改文件描述符标志或文件状态标志时必须谨慎,先要获得现有的标志值,然后按照期望修改它。
1 |
|
1 |
|
较新的系统都提供名为/dev/fd的目录,其目录项为0,1,2,等的链接文件,打开文件/devfd/n等效于复制描述符n。
文件与目录
stat系列函数
1 |
|
这四个函数返回文件相关的结构信息,成功返回0,失败返回-1;
stat函数返回与此命名文件(pathname)有关的信息结构。
fstat函数获得已在描述符fd上打开文件的有关信息。
当文件是一个符号链接时,lastat返回该符号链接有关的信息。
fstatat函数为一个相对于当前打开目录(由fd参数指向)的路径返回文件统计信息。
1 | struct stat { |
文件类型
- 普通文件
- 目录文件(只有内核能写目录)
- 块特殊文件(提供对设备带缓冲的访问,每次访问以固定长度为单位进行)
- 字符特殊文件(对设备提供不带缓冲的访问,每次访问长度可变。)
- FIFO,命名管道
- 套接字,socket
- 符号链接,这种类型的文件指向另一个文件。
文件类型信息包含在stat结构的st_mode成员中。这些宏的参数都是stat结构中的st_mode成员。
| Macro | Type of file |
|---|---|
| S_ISREG() | regular file |
| S_ISDIR() | directory file |
| S_ISCHR() | character special file |
| S_ISBLK() | block special file |
| S_ISFIFO() | pipe or FIFO |
| S_ISLNK() | symbolic link |
| S_ISSOCK() | socket |
1 |
|
用户ID与组ID
| 说明 | |
|---|---|
| 实际用户ID | 我们是谁,在登录时取自口令文件 |
| 实际组ID | |
| 有效用户ID | 用于文件访问权限检查 |
| 有效组ID | |
| 附属组ID | |
| 保存的设置用户ID | 由execl函数保存,有效用户,组ID的副本 |
| 保存的设置组ID |
通常,有效用户ID等于实际用户ID,有效组ID等于实际组ID。
当执行一个程序文件时,进程的有效用户ID通常是实际用户ID,有效组ID通常是实际组ID。但可以在文件模式字(st_mode)中设置一个特殊标记,使执行此文件时,将进程的有效用户ID设置为文件所有者的用户ID(st_uid),将进程的有效组ID设置为文件的组所有者ID(st_gid)。在文件模式字中的这两位被称为设置用户ID(set-user-ID)位和设置组ID(set-group-ID)位。
例如,若文件所有者是超级用户,且设置了该文件的设置用户ID位,那么当该程序文件由一个进程执行时,该进程具有超级用户权限。
设置用户ID位及设置组ID位都包含在文件的st_mode值中,这两位分别用常量S_ISUID和S_ISGID测试。
文件访问权限
| st_mode屏蔽 | 含义 |
|---|---|
| S_IRUSR | 用户(文件所有者)读 |
| S_IWUSR | 用户写 |
| S_IXUSR | 用户执行 |
| S_IRGRP | 组读 |
| S_IWGRP | 组写 |
| S_IXGRP | 组执行 |
| S_IROTH | 其它读 |
| S_IWOTH | 其它写 |
| S_IXOTH | 其它执行 |
如果path环境变量指定了一个我们不具有执行权限的目录,那么shell绝不会在该目录下找到可执行文件。
新文件的用户id设置为进程的有效用户id,组id可以是进程有效组id,也可以是所在目录的组id。
open函数打开一个文件时,内核以进程的有效用户ID和有效组ID为基础执行其访问权限测试。access和faccessat函数是按实际用户ID和实际组ID进行访问权限测试。
1 |
|
umask函数为进程设置文件模式创建屏蔽字,并返回之前的值。cmask参数是(S_IRUSR,S_IWUSR等)9个常量中的若干位按“或”构成。在文件模式创建屏蔽字中为1的位,在文件mode中的相应位一定被关闭。
1 |
|
1 |
|
1 | umask -S //打印符号格式 |
chmod,fchmod与fchmodat这三个函数用于更改现有文件的访问权限。为了更改一个文件的访问权限位,进程的有效用户ID必须等于文件的所有者ID,或者该进程必须具有超级用户权限。
1 |
|
如果对一个目录设置了粘着位,只有对该目录具有写权限的用户并且满足下列条件之一,才能删除或重命名该目录下的文件。
- 拥有该文件
- 拥有此目录
- 是超级用户
函数chown,fchown,fchownat和lchown用于更改文件的用户ID和组ID。
1 |
|
stat结构成员st_size表示以字节为单位的文件的长度,对于符号链接,文件长度就是在文件名中的实际字节数。
文件空洞是由偏移量超过文件尾部后写入数据造成。
剪裁文件长度:
1 |
|
文件系统
我们可以把一个磁盘分成一个或多个分区。每个分区可以包含一个文件系统。i节点是固定长度的记录项,它包含有关文件的大部分信息。

每个i节点都有一个链接计数标记,表示指向该i节点的文件/目录的数量,只有当链接计数标记减少到0时,才会释放该文件所占用的数据块,可以从图中看到有两个目录项指向同一个i节点。

i节点包含了文件的所有信息,包含文件的权限位信息,文件类型文件长度等(文件名保存在目录项中)。
在不更换文件系统的情况下为一个文件重命名,该文件的实际位置并没有更改,只需在目录块中构造一个新的目录项指向原先的i节点,并将原来的目录项删除即可。

创建一个新目录项newpath,它引用现有文件existingpath,如果newpath已存在,则返回出错。只创建newpath中的最后一个分量,路径中的其他部分应当已经存在。
1 |
|
符号链接是对一个文件的间接指针,与硬链接有所不同,硬链接直接指向文件的i节点。硬链接通常要求链接和文件位于同一个文件系统中。只有超级用户才能创建指向目录的硬链接(底层文件支持的情况下)。
1 |
|
文件的时间
| 字段 | 说明 | 例子 | ls(l)选项 |
|---|---|---|---|
| st_atim | 文件数据的最后访问时间 | read | -u |
| st_mtim | 文件数据的最后修改时间 | write | 默认 |
| st_ctim | i节点状态的最后更改时间 | chmod,chowm | -c |
修改时间(st_mtim)是文件内容最后一次被修改的时间。状态更改时间(st_ctim)是该文件的i节点最后一次被修改的时间。影响i节点的操作很多,比如更改文件的访问权限,更改用户ID,更改链接数等。
系统不维护对一个i节点的最后一次访问时间,所有access和stat函数并不更改这3个时间中的任一个。
1 |
|
1 |
|
目录操作
只有内核才能写目录,一个目录的写权限位和执行权限位决定了在该目录中能否创建新文件以及删除文件,并不表示能否写目录本身。
1 |
|
标准IO库
ASCII字符集可以单字节表示,国际字符集可以用多个字节表示。
流的定向(stream’s orientation)决定了所读,写的字符是单字节还是多(宽)字节的。当一个流最初被创建时,没有定向。如果在未定向的流上使用一个多字节I/O函数,则将流的定向设置为宽字节。如果在未定向的流上使用一个单字节I/O函数,则将流的定向设置为字节定向的。
fwide函数可用于设置流的定向。若流是宽定向的,返回正值,字节定向的返回负值,未定向的但会0。
1 |
|
fwide并不改变已定向的流的定向。
- mode参数值为负,fwide试图使指定的流是字节定向的。
- mode参数值为正,fwide试图使指定的流是宽定向的。
- mode参数值为0,fwide不试图设置流的定向,但返回标识该流定向的值。
对一个进程预定义了这三个流,通过预定义文件指针stdin,stdout,stderr加以引用。
标准io提供三种缓冲
- 全缓冲,填满缓冲区后才进行实际io操作,驻留磁盘文件按通常全缓冲,flush说明标准io进行缓冲区的清洗操作,可由标准io自动冲洗,调用fflush可以手动冲洗。
- 行缓冲,在输入输出遇到换行符的受进行io操作,标准io提供的行缓冲区时固定长度,填满缓冲区时没有换行符也进行io操作,当从一个不带缓冲的流或一个行缓冲流得到输入数据,就会重复所有行缓冲流。
- 不缓冲,不对字符进行缓冲存储,标准错误不缓冲。
对任何一个给定的流,如果不喜欢这些系统默认,那么可以更改缓冲类型。这些函数一定要在流已被打开后调用,且在对流执行任何一个其他操作之前调用。
1 |
|
使用vsetbuf可以精确说明缓冲类型:_IOFBF(全缓冲),_IOLBF(行缓冲),IONBF(不带缓冲)。
fflush函数使该流所有未写的数据都被传送至内核。若fp是NULL,则此函数将导致所有输出流被冲洗。
1 |
|
打开关闭流
1 |
|
非格式化I/O流
有三种不同类型的非格式化IO:
- 每次一个字符的IO
- 每次一行的IO
- 直接IO
1 |
|
函数getchar等同于getc(stdin)。getc可被实现为宏,fgetc不能实现为宏。getc的参数不应当具有副作用,它可能被计算多次。put系列函数与get类似。
1 |
|
1 | size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp) ; |
1 |
|
格式化IO
1 |
|
1 |
|
1 |
|
系统数据文件和信息
1 |
|
日期
unix内核提供的基本时间服务是计算自协调时间时UTC公元1970年1月1日00:00:00这一特定时间以来经过的秒数。

1 |
|
clock_gettime函数可用于获取指定时钟的时间。
1 |
|
| 标识符 | 选项 | 说明 |
|---|---|---|
| CLOCK_REALTIME | 实时系统时间 | |
| CLOCK_MONOTONIC | _POSIX_MONOTONIC_CLOCK | 不带负跳数的实时系统时间 |
| CLOCK_PROCESS_CPUTIME_ID | _POSIX_CPUTIME | 调用进程的CPU时间 |
| CLOCK_THREAD_CPUTIME_ID | _POSIX_THREAD_CPUTIME | 调用线程的CPU时间 |
1 |
|
localtime和gmtime函数将日历时间转换分解为struct tm结构。区别在于localtime将日历时间转换为本地时间,gmtime将日历时间转换成协调统一时间。
1 |
|
1 |
|
1 |
|
1 |
|