#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
const char * filename="log.txt";
int main()
{
int fd= open(filename,O_CREAT|O_WRONLY|O_TRUNC,0666);
if(fd<0)
{
perror("open");
return 1;
}
const char*message="hello Linux\n";
write(fd,message,strlen(message));
write(fd,message,strlen(message));
write(fd,message,strlen(message));
write(fd,message,strlen(message));
close(fd);
return 0;
}我们创建一个新文件并写入四行句子

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);这三个函数 stat、fstat 和 lstat 都是 C 语言中用于获取文件的状态信息(如文件大小、权限、修改时间等)的系统调用。它们用于查询文件或目录的元数据,返回一个 struct stat 结构,结构中包含了该文件的详细信息。
这三个函数的区别在于它们如何访问文件,特别是在涉及符号链接(symlinks)时的行为。
stat 函数用于获取指定路径(path)所指向文件或目录的状态信息。它通常用于普通文件、目录或其他类型的文件。
path:指向一个字符串,表示文件或目录的路径。
buf:指向一个 struct stat 结构体,该结构体将被填充上文件的状态信息。
0,并将文件的状态信息存储到 buf 中。
-1,并设置 errno 来指示错误。
用法:
#include<stdio.h>
#include<sys/stat.h>
int main()
{
struct stat file_info;
if(stat("log.txt",&file_info)==-1)
{
perror("stat");
return 1;
}
printf("File size: %ld bytes\n", file_info.st_size);
printf("Permissions: %o\n", file_info.st_mode);
printf("Last modified: %ld\n", file_info.st_mtime);
return 0;
}stat 函数用于获取文件 log.txt 的状态信息,并打印文件的大小、权限和最后修改时间。

fstat 与 stat 很相似,不同之处在于它是通过文件描述符来获取文件的状态,而不是通过路径。它适用于文件已经被打开并且拥有文件描述符的情况。
lstat 函数与 stat 函数非常相似,但它用于获取符号链接本身的状态,而不是符号链接所指向的目标文件的状态。对于普通文件或目录,lstat 的行为与 stat 相同。
用法:
#include <stdio.h>
#include <sys/stat.h>
int main() {
struct stat file_info;
if (lstat("symlink.txt", &file_info) == -1) {
perror("lstat");
return 1;
}
if (S_ISLNK(file_info.st_mode)) {
printf("It's a symbolic link!\n");
} else {
printf("It's not a symbolic link.\n");
}
return 0;
}在这个例子中,lstat 用于获取符号链接 symlink.txt 的状态信息。与 stat 不同,lstat 会返回符号链接本身的元数据,而不是符号链接指向的文件的元数据。如果目标文件是符号链接,stat 会返回链接目标的状态,而 lstat 返回的是符号链接本身的信息。
stat、fstat 和 lstat 的主要区别
函数 | 访问方式 | 适用场景 | 重要差异 |
|---|---|---|---|
stat | 路径(文件名) | 获取指定路径文件的状态 | 适用于普通文件、目录等,符号链接会返回目标文件的状态 |
fstat | 文件描述符 | 获取已打开文件的状态 | 适用于已经通过 open 打开的文件 |
lstat | 路径(文件名) | 获取符号链接本身的状态 | 对于符号链接,返回的是符号链接本身的状态,而非目标文件 |
struct stat 结构体这三个函数都将文件的状态信息存储到 struct stat 结构体中。该结构体包含了关于文件的各种元数据,例如文件的大小、权限、修改时间等。常见的字段如下:
struct stat {
dev_t st_dev; // 设备ID
ino_t st_ino; // inode号
mode_t st_mode; // 文件类型和权限
nlink_t st_nlink; // 硬链接数
uid_t st_uid; // 文件所有者的UID
gid_t st_gid; // 文件所属组的GID
dev_t st_rdev; // 设备类型(如果是设备文件)
off_t st_size; // 文件大小(字节)
blksize_t st_blksize; // 文件系统的块大小
blkcnt_t st_blocks; // 文件占用的块数
time_t st_atime; // 最后访问时间
time_t st_mtime; // 最后修改时间
time_t st_ctime; // 最后状态变化时间
};接着来完成读操作
NAME
read - read from a file descriptor
SYNOPSIS
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);fd:文件描述符,表示要读取的文件或设备。通常,0 表示标准输入(stdin),其他数字表示打开的文件、设备或网络连接。
buf:一个指针,指向程序预先分配的缓冲区,数据会从文件中读取到这个缓冲区。
count:要读取的字节数,即最多读取 count 个字节。
count,表示文件已到达末尾或读取操作受到某些限制。
-1,并设置 errno 来指示错误类型。
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
const char * filename="log.txt";
int main()
{
struct stat st;
int n=stat(filename,&st);
if(n<0)return 1;
int fd= open(filename,O_RDONLY);
if(fd<0)
{
perror("open");
return 2;
}
printf("fd:%d\n",fd);
char *file_buffer=(char*)malloc(st.st_size+1);
n=read(fd,file_buffer,st.st_size);
if(n>0)
{
file_buffer[n]='\0';
printf("%s\n",file_buffer);
}
free(file_buffer);
close(fd);
return 0;
}stat用来获取文件状态,存储在st结构体中,使用 open 系统调用以只读模式打开文件,read(fd, file_buffer, st.st_size):从文件中读取数据,read 会将最多 st.st_size 字节的数据从文件中读取到 file_buffer 中。返回的 n 表示实际读取的字节数。 如果 n > 0,表示成功读取了文件内容,程序会把文件内容输出到屏幕上。file_buffer[n] = ‘\0’; 将读取的数据末尾添加一个结束符,使其成为一个 C 字符串
我们前面提到,文件描述符是从最小开始分配的,分配最小的没有被使用过的fd。
0 1 2是系统默认分配的,我们现在关闭一下观察一下现象
int main()
{
close(0);
int fd = open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("open");
return 1;
}
printf("fd:%d\n",fd);
close(fd);
return 0;
}这行代码关闭了标准输入(stdin,文件描述符 0)。 之后的 open() 调用会返回最小可用的文件描述符

我们现在关闭1:
int main()
{
close(1);
int fd = open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("open");
return 1;
}
printf("printf fd:%d\n",fd);
fprintf(stdout,"fprinf fd:%d\n",fd);
close(fd);
return 0;
}
我们发现这里显示器和文件中都没有打印出内容,这里就与重定向和缓冲区有关了

首先看第一部分,为什么显示器没有内容?
这是因为我们的close(1)关闭了文件标准输出的描述符(stdout,文件描述符 1)
因此,之后所有通过 printf()/fprinf() 输出的内容将不再显示在终端(显示器上),而是会被重定向到指定的文件中
1号此时是我们log.txt的文件描述符,printf依旧向1里面打,所以此时打印的内容打入到了log.txt中,我们这里可以刷新查看:
int main()
{
close(1);
int fd = open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("open");
return 1;
}
printf("printf fd:%d\n",fd);
fprintf(stdout,"fprinf fd:%d\n",fd);
fflush(stdout);
close(fd);
return 0;
}
所以所谓的重定向本质就是在内核中改变文件描述符表,与上层无关
来看第二部分,为什么没有fflush(stdout)这一部分文件就显示不到内容呢?

我们上层往log.txt文件中写的时候,最终是写到了内核文件的缓冲区里面,c语言中,stdin,stdout,stdin这三个本质都是struct FILE*的结构体,这三个对应的底层的文件描述符为0 1 2 ,它们有语言级别的缓冲区,
printf/fprintf并不是直接写入操作系统的,它们都是写入到stdout语言级别的缓冲区里,后面stdout通过1号文件描述符刷新到操作系统的文件缓冲区里,此时外设才能看到缓冲区的内容
所以fflush传参stdout本质不是把底层内核文件缓冲区刷到外设上,而是把语言级别的缓冲区,通过文件描述符,写到内核当中
我们代码最后直接close(fd),这里fd是我们打开的设备,所以我们正准备return之前刷新的时候,直接把文件描述符关了,将来刷新是根本没有办法通过1写入文件中,所以最终我们看见log.txt中没有任何内容
所以这里fflush在文件关之前刷新到了文件中
dup2 是 Linux/Unix 下的一个 系统调用,用于将一个文件描述符(fd_old)复制到 另一个文件描述符(fd_new)。如果 fd_new 已经被打开,dup2 会先关闭它,然后让 fd_new 指向 fd_old 指向的文件。本质是文件描述符下标所对应内容的拷贝
#include <unistd.h>
int dup2(int fd_old, int fd_new);fd_old:要复制的文件描述符(源)。fd_new:目标文件描述符(目的地)。fd_new。-1,并设置 errno。特点:
fd_new 会被强制指向 fd_old 所指的文件。fd_new 已经打开,dup2 会先关闭 fd_new,然后再进行复制。fd_new 和 fd_old 共享同一个文件表项(即共享偏移量、文件状态等),但它们是独立的文件描述符。让标准输出重定向到文件
dup2 最常见的用途之一是 重定向标准输入 (stdin)、标准输出 (stdout) 或标准错误 (stderr),通常用于日志文件、命令行工具或守护进程。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 1;
}
dup2(fd, 1); // 把标准输出 (fd=1) 重定向到 log.txt
close(fd); // 关闭 fd,标准输出仍然有效
printf("This will be written to log.txt\n");
return 0;
}解释:
log.txt,获取文件描述符 fd(比如 fd = 3)。dup2(fd, 1); 让 stdout(fd = 1)指向 fd = 3 的文件。fd = 3,但 stdout 仍然指向 log.txt。printf() 现在不会输出到终端,而是写入 log.txt。

让标准错误 (stderr) 也写入文件
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("error.log", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 1;
}
dup2(fd, 2); // 把标准错误 (fd=2) 重定向到 error.log
close(fd);
fprintf(stderr, "This is an error message!\n");
return 0;
}效果:
fprintf(stderr, "..."); 输出都会进入 error.log,而不会显示在终端。创建子进程并修改输入/输出
在 进程创建后,子进程继承了父进程的文件描述符。如果我们希望子进程的 stdin 或 stdout 进行重定向,可以使用 dup2。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 1;
}
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid == 0) { // 子进程
dup2(fd, 1); // 重定向 stdout 到 output.txt
close(fd);
execlp("ls", "ls", "-l", NULL); // `ls -l` 输出将写入 output.txt
perror("execlp");
exit(1);
}
close(fd);
wait(NULL); // 等待子进程结束
return 0;
}执行流程:
output.txt 并获取文件描述符 fd。fork() 生成子进程,子进程继承 fd。dup2(fd, 1); 让 stdout 指向 output.txt。execlp("ls", "ls", "-l", NULL); 执行 ls -l 命令,输出写入 output.txt。ls -l 的输出,而 output.txt 里会有 ls -l 的结果。使用 dup2 进行进程间通信
如果两个进程使用 pipe() 创建管道,dup2 可以让子进程的 stdin/stdout 连接到管道。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int pipefd[2];
pipe(pipefd);
pid_t pid = fork();
if (pid == 0) { // 子进程
close(pipefd[0]); // 关闭管道读取端
dup2(pipefd[1], 1); // 让 stdout 指向管道写入端
close(pipefd[1]);
execlp("ls", "ls", "-l", NULL); // `ls -l` 输出进入管道
perror("execlp");
exit(1);
}
close(pipefd[1]); // 关闭管道写入端
char buffer[1024];
read(pipefd[0], buffer, sizeof(buffer)); // 读取子进程的输出
printf("Output from child process:\n%s\n", buffer);
close(pipefd[0]);
return 0;
}作用:
ls -l,但 stdout 被 dup2 重定向到管道。read() 读取子进程的 ls -l 结果,并打印到终端。特性 | dup(fd) | dup2(fd_old, fd_new) |
|---|---|---|
作用 | 复制 fd 到一个新的最小可用 fd | 强制将 fd_new 指向 fd_old |
fd_new | 自动分配新 fd | fd_new 由用户指定 |
关闭 fd_new | 否 | 是(会关闭 fd_new,然后再复制) |
返回值 | 新的 fd | fd_new |
示例:
int new_fd = dup(fd); // 自动分配新的文件描述符
dup2(fd, 4); // 让 4 指向 fd✅ dup2(fd_old, fd_new) 让 fd_new 指向 fd_old
✅ 应用场景:
exec 前修改 I/Opipe() + dup2)缓冲区(Buffer) 本质上是一个临时存储数据的内存区域,用于提高 I/O 处理的效率,减少系统调用的次数。
为什么需要缓冲区
在计算机系统中,数据的读写速度通常是不均衡的:
如果每次读写数据都直接操作外部设备(比如磁盘或网络),CPU 可能会因为等待 I/O 而浪费大量时间。因此,缓冲区的作用是让数据的读写更高效,减少直接访问外部设备的次数。
缓冲区的分类
缓冲区可以按作用场景分为多种类型:
缓冲区类型 | 作用 |
|---|---|
用户态(应用层)缓冲区 | C 标准库 stdio 缓冲区(如 stdout、stdin),减少 write() 调用,提高性能 |
内核态缓冲区(操作系统层) | page cache(磁盘缓存)、socket buffer(网络缓冲) |
设备缓冲区 | 硬盘、网卡、打印机等设备内部的缓冲 |
环形缓冲区(Ring Buffer) | 常见于音视频处理、网络通信 |
stdio 的缓冲区(1)C 语言的 stdout 其实有缓冲
在 C 语言中,printf() 并不会立即把数据写入屏幕或文件,而是先存入 stdout 语言级别的缓冲区,然后由 fflush(stdout) 或 \n 触发输出。
示例
#include <stdio.h>
int main() {
printf("Hello, World!"); // 没有 `\n`,可能不会立刻显示
while (1); // 进入死循环,不调用 `fflush(stdout)`
}可能的现象:
Hello, World!,因为 stdout 还没有刷新。fflush(stdout);\n,行缓冲模式会自动刷新setbuf(stdout, NULL);stdout 的 3 种缓冲模式C 语言的 stdio(stdout, stdin, stderr)在不同情况下有不同的缓冲模式:
缓冲模式 | 触发时机 | 应用场景 |
|---|---|---|
全缓冲(Fully Buffered) | 缓冲区满了时 或 fflush(stdout); | 文件 I/O |
行缓冲(Line Buffered) | 遇到 \n 时自动刷新 | 终端交互(如 stdout) |
无缓冲(Unbuffered) | 每次 printf() 都直接写入 | stderr(标准错误) |
修改 stdout 缓冲模式
#include <stdio.h>
int main() {
setbuf(stdout, NULL); // 禁用缓冲区(无缓冲)
printf("Hello, World!"); // 立刻输出
}write() vs. printf()
函数 | 是否经过 C 语言缓冲区 | 是否直接写入内核 |
|---|---|---|
printf() | ✅ 是 | ❌ 否(写入 stdio 缓冲区) |
fprintf(stdout, ...) | ✅ 是 | ❌ 否(写入 stdout 缓冲区) |
fflush(stdout) | ✅ 是 | ✅ 是(写入内核 write()) |
write(fd, buf, size) | ❌ 否 | ✅ 是(直接进入内核缓冲区) |
即使 printf() 经过 fflush(stdout);,或者 write(fd, buf, size);,数据仍然不会立即写入磁盘,而是进入内核的 Page Cache,等待操作系统调度落盘。
(1)Page Cache 的作用
I/O 操作次数。(2)如何强制数据写入磁盘?
使用 fsync(fd);
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
write(fd, "Hello, World!", 13);
fsync(fd); // 强制写入磁盘
close(fd);
return 0;
}fsync(fd); 强制把 page cache 数据写入磁盘。
fflush(stdout); vs. fsync(fd);
函数 | 作用 | 刷新的范围 |
|---|---|---|
fflush(stdout); | 刷新 C 语言 stdio 缓冲区 | 从 stdout 到 write(fd, buf, size); |
fsync(fd); | 刷新内核 Page Cache | 从 Page Cache 到 磁盘 |
进程通信(IPC)中也大量使用缓冲区:
pipe() 读写时,数据先写入 内核管道缓冲区,再由 read() 读取。✅ 缓冲区的作用
✅ 缓冲区的层次
层次 | 缓冲区类型 |
|---|---|
C 语言缓冲区 | stdout, stderr, stdin |
内核缓冲区 | page cache, socket buffer |
设备缓冲区 | 硬盘、网卡、打印机 |
✅ 如何控制缓冲区刷新
fflush(stdout); 把 stdio 数据写入 write()fsync(fd); 把 write() 发送的数据刷入磁盘O_SYNC / O_DIRECT 直接绕过 Page Cache✅ write() vs. printf()
write(fd, buf, size); 直接进入内核,不会受 fflush() 影响。printf() 先写入 C 语言缓冲区,需要 fflush(stdout); 才能写入 write()。🌟 重点:
C 语言的 stdout 缓冲区和 Linux Page Cache 是两层不同的缓冲区,fflush(stdout); 只能刷新 stdout,但不会保证数据写入磁盘,需要 fsync(fd);。 🚀
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int main()
{
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
const char *msg ="hello write\n";
write(1,msg,strlen(msg));
fork();
return 0;
}我们运行结果和重定向到log.txt打印结果不同

log.txt打印两次肯定与fork()有关,./myfile默认是向显示器打印的,显示器的刷新策略是行刷新,重定向的本质是向普通文件进行写入,这里的刷新策略发生变化,普通文件采用的是全缓冲
如果是向显示器打印,还没走到fork,上面打印数据已经写到了操作系统内部,文件缓冲区里数据已经存在了,这里的fork没什么意义了
但是重定向到文件中,它是全缓冲,文件的缓冲区并没有被写满,文件的缓冲区会将写入的数据暂时的保存起来,但是write系统调用直接写到了内核里,后面在fork时,write已经写到了操作系统内部,但是printf和fprintf依旧在语言级别的stdout的缓冲区中,所以fork时候数据还在缓冲区中,因为缓冲区没写满,所以fork这里出现父子进程,退出的时候父子进程各自刷新一次缓冲区,所以printf和fprintf打印两次
