0x09
Unix I/O
一个Linux文件就是一个m个字节的序列。所有的I/O设备都被模型化为文件,所有的输入和输出都被当做相应文件的读和写来执行。
- 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
- Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符0)、标准输出(描述符1)和标准错误(描述符2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,可迎来代替显式的描述符值。
- 改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序通过执行seek操作,显式地设置文件的当前位置为k。
- 读写文件。一个读操作就是从文件复制n > 0个字节到内存,从当前文件位置k开始,然后将k增加到k + n。给定一个大小为m字节的文件,当k >= m时执行读操作会触发一个称为end-of-file(EOF)的条件。写操作就是从内存复制n > 0个字节到一个文件,从当前文件位置k开始,然后更新k。
- 关闭文件。当应用完成了对文件的访问后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
文件
每个Linux文件都有一个类型来表明它在系统中的角色:
-
普通文件。包含任意数据。应用程序常常要区分文本文件和二进制文件,文本文件是只含有ASCII或Unicode字符的普通文件;二进制文件是所有其他的文件。对内核而言,文本文件和二进制文件没有区别。
Linux文本文件包含了一个文本行序列,其中每一行都是一个字符序列,以一个新行符("\n")结束。
-
目录是包含一组链接的文件,其中每个链接都将一个文件名映射到一个文件,这个文件可能是另一个目录。每个目录至少含有两个条目:"."是到该目录自身的链接,".."是到目录层次结构中父目录的链接。
-
套接字是用来与另一个进程进行跨网络通信的文件。
打开和关闭文件
#include <sys/types.h> #include <sys/stat.h> #include <fchtl.h> int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件。
- O_RDONLY:只读。
- O_WRONLY:只写。
- O_RDWR:可读可写。
flags参数也可以是一个或者更多位掩码的或,为写提供给一些额外的指示:
- O_CREAT:如果文件不存在,就创建它的一个截断的(空)文件。
- O_TRUNC:如果文件已经存在,就截断它。
- O_APPEND:在写操作前,设置文件位置到文件的结尾处。
mode参数指定了新文件的访问权限位。作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置为mode & ~umask。
#define DEF_MODE S_IRUSR | SIWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH #define DEF_UMASK S_IWGRP | S_IWOTH umask(DEF_UMASK); fd = open("foo.txt", O_CREAT | O_TRUNC | O_WRONLY, DEF_MODE);
此时文件的拥有者有读写权限,而所有其他的用户都有读权限。
#include <unistd.h> int close(int fd);
读和写文件
应用程序是通过分别调用read和write函数来执行输入和输出的。
#include <unistd.h> ssize_t read(int fd, void *buf, size_t n); ssize_t write(int fd, const void *buf, size_t n);
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。write若成功则返回写的字节数,若出错则为-1。
write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。
在某些情况下,read和write传送的字节比应用程序要求的少,这些不足值不表示有错误。出现这样情况的原因有:
- 读时遇到EOF。
- 从终端读文本行。如果打开文件是与终端相关联的(如键盘和显示器),那么每个read函数将一次传送一个文本行,返回的不足值等于文本行的大小。
- 读和写网络套接字。如果打开的文件对应于网络套接字,那么内部缓冲约束和较长的网络延迟会引起read和write返回不足值。
读取文件元数据
应用程序能够通过调用stat和fstat函数,检索到关于文件的信息(文件的元数据)。
#include <unistd.h> #include <sys/stat.h> int stat(const char *filename, struct stat *buf); int fstat(int fd, struct stat *buf);
stat函数以一个文件名作为输入,并填写一个stat数据结构中的各个成员。
struct stat { dev_t st_dev; ino_t st_ino; mode_t st_mode; nlink_t st_nlink; uid_t st_uid; gid_t st_gid; dev_t st_rdev; off_t st_size; unsigned long st_blksize; unsigned long st_blocks; time_t st_atime; time_t st_mtime; time_t st_ctime; };
st_size成员包含了文件的字节数大小。st_mode成员则编码了文件访问许可位和文件类型。Linux在sys/stat.h中定义了宏谓词来确定st_mode成员的文件类型:
S_ISREG(m) 是否普通文件 S_ISDIR(m) 是否目录文件 S_ISSOCK(m) 是否网络套接字
读取目录内容
应用程序可以用readdir系列函数来读取目录的内容。
#include <sys/types.h> #include <dirent.h> DIR *opendir(const char *name);
opendir以路径名为参数,返回指向目录流的指针。流是对条目有序列表的抽象,在这里是指目录项的列表。
#include <dirent.h> struct dirent *readdir(DIR *dirp);
每次对readdir的调用返回的都是指向流dirp中下一个目录项的指针。如果没有更多目录项则返回NULL。每个目录项都是一个结构:
struct dirent { ino_t d_ino; char d_name[256]; }
d_name是文件名,d_ino是文件位置。如果出错,则readdir返回NULL,并设置errno。
#include <dirent.h> int closedir(DIR *dirp);
共享文件
内核用三个相关的数据结构来表示打开的文件:
- 描述符表。每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。
- 文件表。打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。每个文件表的表项组成包括当前的文件位置、引用计数,以及一个指向v-node表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的引用计数。内核不会删除这个文件表表项,直到它的引用计数为零。
- v-node表。同文件表一样,所有的进程共享这张v-node表。每个表项包含stat结构中的大多数信息,包括st_mode和st_size成员。
多个描述符可以通过不同的文件表表项来引用同一个文件。如果以同一个filename调用open函数两次,就会发生这种情况。关键思想是每个描述符都有它自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据。
子进程有一个父进程描述符表的副本。父子进程共享相同的打开文件表集合,因此共享相同的文件位置。一个很重要的结果就是,在内核删除相应文件表表项之前,父子进程必须都关闭了它们的描述符。
I/O重定向
Linux shell提供了I/O重定向操作符,允许用户将磁盘文件和标准输入输出联系起来。
linux> ls > foo.txt
I/O重定向一种方式是使用dup2函数。
#include <unistd.h> int dup2(int oldfd, int newfd);
dup2函数复制描述符表项oldfd到描述符表表项newfd,覆盖描述符表项newfd以前的内容。如果newfd已经打开了,dup2会在复制oldfd之前关闭newfd。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)