系统级I/O
输入/输出(I/O)是主存和外部设备之间复制数据的过程。
输入:从I/O设备复制数据到主存。
输出:主存复制数据到I/O设备
所有语言的运行时系统都提供执行I/O的较高级别的工具。例如:ANSI C提供标准I/O库,包含像printf和scanf这样执行带缓冲区的I/O函数。
在Linux系统中,是通过使用由内核提供的系统级Unix I/O函数来实现较高级别的I/O函数的。
文件#
在Linux系统中,文件可以看作字节序列,且所有I/O设备都是用文件来表示,甚至连内核都是用文件表示。
因为I/O设备也是文件,所以内核可以利用Unix I/O的简单接口来处理输入输出。
如何区别不同的文件类型呢?
每个Linux文件都有一个类型(type
)来表明它在系统中的角色:
- 普通文件(regular file)
- 目录(directory)
- 套接字(socket)
- 其他文件类型:有名管道(named pipe)、符号链接(symbolic link)、字符和块设备(character and block device)
普通文件#
普通文件包含任意数据。
应用程序:需要区分文本文件(text file)和二进制文件(binary file)
- 文本文件:只含有ASCII或Unicode字符的普通文件。是一系列的文本行。
- 二进制文件:除文本文件外全部是二进制文件
内核:文本文件和二进制文件没有区别
目录#
目录是包含一组链接(link)的文件,其中每个link都将一个文件名(filename)映射到一个文件,这个文件可能是一个目录。
每个目录至少包含两个条目:.
和..
Linux内核将所有文件都组织成一个目录层次结构,由名为/的根目录确定。
内核会为每个进程保存当前工作目录(cwd),可以使用cd命令来更改
套接字#
套接字:用来和另一个进程进行跨网络通信的文件。
打开文件#
NAME
open, openat, creat - open and possibly create a file
SYNOPSIS
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
RETURN VALUE
open() return the new file descriptor (a nonnegative integer),
or -1 if an error occurred (in which case, errno is set appropriately).
flags参数:进程打算如何访问文件
O_RDONLY: 只读
O_WRONLY: 只写。
O_RDWR: 可读可写。
以上只能出现一个
flags也可以是一个或更多位掩码的或,为写提供一些额外的指示
O_CREAT: 如果文件不存 ,就创建它的一个截断的 (truncated) (空)文件
O_TRUNC: 如果文件巳经存在,就截断它
O_APPEND: 在每次写操作前,设置文件位置到文件的结尾处
若有O_CREAT
参数,需要添加mode参数,指定了新文件的访问权限位。
在Linux系统中,umask用于确定新创建文件和目录权限的设置。每一个进程都有一个umask。当进程创建文件时,会用mode&~umask
对文件进行权限设置
返回值:一个最小的整数文件描述符(file descriptor)。错误-1
由于每个Linux Shell创建的时候都会默认打开三个文件(stdin/stdout/stderr)
关闭文件#
每open
一个文件,都需要使用close
来关闭文件
#include <unistd.h>
int close(int fd);
关闭一个已关闭的描述符会出错。因此,在close文件时,一定要进行检查
读取文件#
打开和关闭文件之间就是读取文件,实际上是将文件中对应的字节复制到内存中,并更新文件指针。
ssize_t read(int fd, void *buf, size_t n);
成功:返回成功读取的字节数,EOF则为0,nbytes < 0 出错:-1
但nbytes < sizeof(buf)
这种情况(short counts)发生不会出错。
写入文件#
write函数从内存位置buf复制至多n个字节到fd的当前文件位置。
ssize_t write(int fd, const void *buf, size_t n);
成功:实际写入的字节数。出错-1
前面提到的short count
出现的原因有:
在读的时候遇到EOF
从终端中读取文本行
读取和写入网络
也有可能在下面的情况下发生:
从磁盘文件中读取(除EOF外)
写入到磁盘文件
读取文件元信息#
应用程序可以调用stat
和fstat
函数,检索到关于关于文件的源数据
int stat(const char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf);
成功:0;
出错:-1;
fstat
函数以一个文件描述符作为输入,并填写下面的一个stat
结构体
struct stat
{
dev_t st_dev; // Device
ino_t st_ino; // inode
mode_t st_mode; // Protection & file type
nlink_t st_nlink; // Number of hard links
uid_t st_uid; // User ID of owner
gid_t st_gid; // Group ID of owner
dev_t st_rdev; // Device type (if inode device)
off_t st_size; // Total size, in bytes
unsigned long st_blksize; // Blocksize for filesystem I/O
unsigned long st_blocks; // Number of blocks allocated
time_t st_atime; // Time of last access
time_t st_mtime; // Time of last modification
time_t st_ctime; // Time of last change
}
在这个数据结构中,需要注意几个信息,分别为:
st_ino
: 磁盘号
st_mode
: 文件的类型,文件权限
st_nlink
: 文件的硬链接数
st_size
:文件的字节数大小
Linux在sys/stat.h
中定义了一系列宏来确定st_mode
的文件类型
S_ISREG(mode)
: 是普通文件吗
S_ISDIR(mode)
: 是目录文件吗
S_ISSOCK(mode)
: 是网络套接字吗
S_ISLNK(mode)
: 是符号链接吗
int main (int argc, char **argv)
{
struct stat stat;
char *type, *readok;
Stat(argv[1], &stat);
if (S_ISREG(stat.st_mode)) // 确定文件类型
type = "regular";
else if (S_ISDIR(stat.st_mode))
type = "directory";
else
type = "other";
if ((stat.st_mode & S_IRUSR)) // 检查读权限
readok = "yes";
else
readok = "no";
printf("type: %s, read: %s\n", type, readok);
exit(0);
}
读取目录内容#
在读取目录内容之前需先试用opendir
打开目录。以路径名为输入,返回指向目录流(directory stream)的指针。流是对条目有序列表的抽象,这里是指目录项的列表。
#include <sys/types.h>
#fnclude <dirent.h>
DIR *opendir(const char *name);
返回 若成功 则为处理的指针 ;若出错 ,则 NULL
应用程序可以用readdir
函数读取目录内容
SYNOPSIS
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
返回:成功,指向下一个目录项的指针
没有更多的目录项 / 出错,则为NULL
若出错,会设置errno,因此,可以更具errno来判断是否出错
每个目录项都是一个结构,其形式如下:
struct dirent {
ino_t d_ino; // 文件位置
char d_name[256]; // 文件名字
};
虽然有些Linux系统在dirent
结构体中包含其他成员,但只有这两个对其他系统来说都是标准的
下面的示例描述如何使用readdir
#include <func.h>
int main(int argc, char* argv[])
{
// 1. 打开目录流
DIR *pdir = opendir(argv[1]);
if (pdir == NULL) {
error(1, errno, "opendir");
}
// 2. 遍历目录流
errno = 0;
struct dirent *curr;
while ((curr = readdir(pdir)) != NULL){
// 打印目录信息
printf("d_ino = %lu, d_name = %s", curr->d_ino, curr->d_name);
} // curr == NULL
if (errno) {
error(1, errno, "readdir");
}
// 3. 关闭目录流
closedir(pdir);
return 0;
}
共享文件#
可以用很多方式来共享Linux文件。那么Linux内核是如何表示打开文件的呢?
实际上,Linux内核使用了三个相关的数据结构来表示打开的文件:
-
文件描述符表(file descriptor table)。每个进程都有自己独立的描述符表,它的表项由进程打开的文件描述符来索引。每个文件描述符指向文件表中的一个表项。
-
文件表(file table)。打开文件的集合是由一张文件表来表示,所有进程共享这张文件表。
- 文件表的表项:
- 当前文件的位置(current file offset)、
- 引用计数(reference count)、
- 以及一个指向
v-node
表中对应表项的指针(vnode pointer)。
- 文件表的表项:
-
vnode表(vnode table)。所有进程共享。在该表中包含stat结构体中的大多数信息,

下图是一个典型的情况,没有共享文件,并且每个文件描述符对应不同的文件

多个文件描述符指向同一个文件

父子进程是如何共享文件的呢?
假设在fork
之前,父进程有图10-12所示的打开文件。图10-14展示了调用fork
后的情况。子进程有父进程文件描述符表的副本。父子进程共享相同的打开文件表集合,因此,共享相同的打开文件。

假设磁盘文件foobar.txt由6个ASCII码字符组成“foobar”
int main()
{
int fd1, fd2;
char c;
fd1 = Open("foobar.txt", O_RDONLY, O);
fd2 = Open("foobar.txt", O_RDONLY, 0);
Read(fd1, &c, 1);
Read(fd2, &c, 1);
printf("c = %c\n", c);
exit(O);
}
打印:c = f
为什么会打印c = f
呢,fd1和fd2都有自己的打开文件表,,所以每个描述符对千 fo bar.txt 都有它自己的文件位置 。因此,fd2的读操作会读取foobar.txt的第一个字节
int main()
{
int fd;
char c;
fd = Open("foobar.txt", O_RDONLY, O);
if (Fork() == 0) {
Read(fd, &c, 1);
exit(O);
}
Wait(NULL);
Read(fd, &c, 1);
printf("c = %c\n", c);
exit(O);
}
打印:c = o
I/O重定向#
Linux shell提供了I/O重定向操作符。允许用户将磁盘文件和标准输入输出联系起来。
Linux:ls > foo.txt
使得 shell 加载和执行 ls 程序,将标准输出重定向到磁盘文件 foo.txt。
那么I/O重定向是如何工作的呢?一种方式是使用dup2
函数
#include <unistd.h>
int dup2(int oldfd, int newfd);
返回:成功,为非负的描述符
-1 出错
dup2
函数复制描述符表项oldfd
到newfd
,覆盖描述符表项newfd
之前的内容。如果newfd
已经打开,dup2
会在复制oldfd
之前关闭newfd
。
假设在调用dup(4,1)
之前,状态如图10-12所示。
其中fd1
(标准输出)对应文件A(比如一个终端),
fd4
对应文件B(比如一个磁盘文件)。A和B的引用计数都等于1。
图10-15显示调用dup2(4,1)
之后的情况。
两个描述符都指向文件B;
文件A被关闭,且它的文件表和vnode表项都已经被删除
文件b的引用计数增加。

int main(argc, argv[])
{
int fd1, fd2;
char c;
fd1 = Open("foobar.txt", O_RDONLY, O);
fd2 = Open("foobar.txt", O_RDONLY, O);
Read(fd2, &c, 1);
Dup2(fd2, fdl);
Read(fd1, &c, 1);
printf("c = %c\n", c);
exit(O);
}
打印 c = o
这是因为在第一次read后会修改文件打开表中当前文件的位置。重定向会指向相同的文件打开表。因此会输出c
标准I/O#
C语言定义了一组高级输入输出函数,称为标准I/O库,为程序员提供 Unix I/O的较高级别的替代。
在这个库中,提供了一系列函数操作文件
- 打开和关闭文件:
fopen
,fclose
- 读取和写入字节:
fread
,fwrite
- 读取和写入行:
fgets
,fputs
- 格式化读取和写入:
fscanf
,fprintf
标准I/O库将一个打开的文件模型化为一个流。对程序员来说,一个流就是一个指向FILE类型的指针。FILE类型的流实际上是对文件描述符和缓冲区在内存中的抽象。每个C程序开始时都有三个打开的流 stdin stdout、stderr
#include <stdio.h>
extern FILE *stdin; // 标准输入 descriptor 0
extern FILE *stdout; // 标准输出 descriptor 1
extern FILE *stderr; // 标准错误 descriptor 2
int main()
{
fprintf(stdout, "Hello, Da Wang\n");
}
为什么需要缓冲区呢?
其目的是使开销较高的Linux I/O系统调用的数量尽可能得小。
程序经常会一次读入或者写入一个字符,比如 getc
, putc
,也可能会一次读入或者写入一行,比如 gets
, fgets
。如果用 Unix I/O 的方式来进行调用,是非常昂贵的,比如说 每次使用read
和 write
一个字符的读或写,因为需要内核调用,代价相当的昂贵。
解决的办法就是利用 read
函数一次读取一块数据,然后再由高层的接口,一次从缓冲区读取一个字符(当缓冲区用完的时候需要重新填充)
标准 C I/O 提供了带缓存访问文件的方法,使用的时候几乎不用考虑太多,但是如果要获取文件元信息,就需使用 Unix I/O 中的 stat
函数。
I/O函数的正确使用(待补充)#
只要有可能就使用标准I/O。对磁盘和终端设备I/O来说,标准I/O函数是首选方法。
对网络套接字设备的I/O使用Unix I/O。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!