系统文件I/O
文章目录
- 1. open函数
- 1.1 open函数介绍
- 1.2 flag
- 1.3 mode
- 2. write函数
- 2.1 write函数介绍
- 2.2 buf
- 3. read函数
- 3.1 read函数介绍
- 4. 文件描述符 fd
- 4.1 引入
- 4.2 FILE
- 4.3 fd
- 5. 重定向原理
- 6. dup2
- 7. 总结
1. open函数
1.1 open函数介绍
打开⽂件的⽅式不仅仅是fopen,ifstream等流式,这些都是语⾔层的⽅案。其实系统才是打开⽂件最底层的⽅案。而系统则通过open函数来打开文件。通过二号手册可以查到open函数。

参数介绍:
pathname:打开文件的路径。flags:文件标志位(打开文件的方式)mode:文件的权限。
返回值:返回文件描述符。
1.2 flag
flag参数就是标志位,有以下选项:
O_RDONLY:只读模式打开
O_WRONLY:只写模式打开
O_RDWR:读写模式打开
O_CREAT:如果文件不存在则创建它
O_EXCL:与O_CREAT一起使用,如果文件已存在则返回错误
O_TRUNC:如果文件存在且以可写模式打开,则将文件长度截断为 0
O_APPEND:以追加模式打开,写入的数据会添加到文件末尾
open是系统调用函数,其flags标志位本质上是通过比特位进行检查和组合的。采用位图来传标志位,每个标志对应一个特定的比特位,通过按位运算实现多标志的组合与判断。
示例如下:
#include <stdio.h>
#define ONE_FLAG (1<<0) // 0000 0000 0000...0000 0001
#define TWO_FLAG (1<<1) // 0000 0000 0000...0000 0010
#define THREE_FLAG (1<<2) // 0000 0000 0000...0000 0100
#define FOUR_FLAG (1<<3) // 0000 0000 0000...0000 1000void Print(int flags)
{if(flags & ONE_FLAG){printf("One!\n");}if(flags & TWO_FLAG){printf("Two\n");}if(flags & THREE_FLAG){printf("Three\n");}if(flags & FOUR_FLAG){printf("Four\n");}
}int main()
{Print(ONE_FLAG);Print(ONE_FLAG | TWO_FLAG);Print(ONE_FLAG | TWO_FLAG | THREE_FLAG);Print(ONE_FLAG | TWO_FLAG | THREE_FLAG);return 0;
}
每个标志通过左移运算(1<<n)定义,确保每个标志只占用一个独立的比特位(值为 2 的幂)。
这样定义的好处是:多个标志可以通过按位或(|)组合,且彼此不会冲突。
1.3 mode
mode:当使用 O_CREAT 标志创建新文件时,必须指定新文件的权限。mode参数的意义就是指定权限。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>int main()
{// umask(0) // 设置权限掩码int fd = open("log.txt", O_CREAT | O_WRONLY, 0666);if(fd < 0){perror("open");return 1;}return 0;
}

我们设置了666的权限,但是创建出来的文件去不是666权限,是因为在我们的系统中还存在权限掩码umask的影响。如果我们不想受系统的权限掩码影响,只需要在第一行设置一下权限掩码,系统就会根据就近原则帮我们创建文件。
2. write函数
2.1 write函数介绍
和open函数一样,write函数也是系统级别的函数,我们在语言层面用到的fwrite等函数底层都封装了write函数。

参数介绍:
fd:文件描述符。buf指向缓冲区的指针,存储要写入的数据(即待写入的内容)。count要写入的字节数(size_t类型,无符号整数)。
返回值:返回实际写入的字节数,若写入失败则返回-1
示例如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{umask(0);int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666); if(fd < 0){perror("open");return 1;}printf("%d\n", fd);const char* msg = "hello linux\n";int cnt = 5;while(cnt--){write(fd, msg, strlen(msg));}close(fd);return 0;
}
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
解释:创建文件,以写方式打开,并且每次打开清空文件。
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
解释:创建文件,以写方式打开,并且以追加方式写入。
通过这个例子更好的理解通过位图传标志位,并且用或运算符增加文件操作的灵活性。理解语言层,和系统层的联系。
2.2 buf
Q:我注意到write函数的第二个参数buf的类型是void,底层难道不用管写入的内容类型吗?
A:是的,系统实际上是不关心我们的写入方式,你写整型,系统就将整型原封不动的写入文件,然后以二进制存储,当我们读文件的时候编辑器按照二进制给我们翻译。如果你是直接将整型写入文件,这时候就有问题了,编辑器会将对应字符的ASCLL码显示,导致产生乱码。

示例如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{umask(0);int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); if(fd < 0){perror("open");return 1;}printf("%d\n", fd);int num = 12345;int cnt = 5;while(cnt--){char buffer[16];snprintf(buffer, sizeof(buffer), "%d", num);write(fd, buffer, strlen(buffer));// write(fd, &num, sizeof(num));}close(fd);return 0;
}
Q:系统向文件写入时是以二进制写入吗?
A:是的。
write系统调用会原封不动地将内存缓冲区中的字节写入文件,不会对数据做任何编码或转换。 具体到向log.txt写入字符串msg的场景,需要区分两种情况:
- 如果
msg是字符串(如char *msg = "hello log") 此时写入的是字符串的ASCII/UTF-8编码的二进制字节流。例如字符串"abc"在内存中是0x61 0x62 0x63(ASCII码的二进制表示),write会直接将这三个字节写入文件。 因为文本文件(.txt)本质上就是由字符的二进制编码组成的,所以用write写入字符串后,用文本编辑器打开log.txt能正常显示为文字(编辑器会自动将二进制字节解析为字符)。- 如果
msg是二进制数据(如整数、结构体等) 例如写入一个整数12345(二进制表示为0x30 0x39),write会直接写入这两个字节。此时用文本编辑器打开log.txt可能会显示乱码(因为编辑器会尝试将二进制字节解析为字符,而这些字节可能不对应可打印字符)。- 总结
write的写入方式始终是二进制(直接操作字节)。 写入后文件内容是否为 “可阅读的文本”,取决于你写入的数据本身是否是字符的编码(如ASCII、UTF-8等)。对于日志文件log.txt,通常我们会写入字符串(字符编码的二进制),因此最终能以文本形式查看。
在此又理解了snprintf的意义,要通过格式控制,先将整数写入字符数组,再将字符写入文件。
3. read函数
3.1 read函数介绍
read函数同样是系统调用函数,不再赘述。

参数介绍:
fd文件描述符,表示要读取数据的源。buf指向缓冲区的指针,用于存储读取到的数据(需提前分配足够空间)。count期望读取的最大字节数(size_t类型,无符号整数)。
返回值:返回实际读取的字节数,读失败返回-1,读到文件末尾返回0
示例如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{umask(0);int fd = open("log.txt", O_RDONLY); if(fd < 0){perror("open");return 1;}printf("%d\n", fd);while(1){char buffer[64];int n = read(fd, buffer,sizeof(buffer) - 1);if(n > 0){buffer[n] = 0;printf("%s", buffer);}else if(n == 0)break;}close(fd);return 0;
}
int n = read(fd, buffer,sizeof(buffer) - 1);
解释:从文件描述符fd中读取数据到buffer缓冲区中,每次最多读sizeof(buffer) - 1)个字节。返回值n表示读到的字节个数。
4. 文件描述符 fd
4.1 引入
Q:fd是什么,为啥这三个函数都和fd息息相关?为啥通过一个整数就能访问文件?
A:fd是文件描述符。
示例如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{int fd1 = open("log1.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd2 = open("log2.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd3 = open("log3.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd4 = open("log4.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);printf("fd1:%d\n", fd1);printf("fd2:%d\n", fd2);printf("fd3:%d\n", fd3);printf("fd4:%d\n", fd4);close(fd1);close(fd2);close(fd3);close(fd4);return 0;
}

Q: 为什么从3开始打印?012去哪里了?
A: 012 表示标准输入,标准输出,标准错误。
要真的弄清楚文件描述符,我们要先弄清楚我们之前学的FILE
4.2 FILE

Q:FILE是什么?
A:是C语言提供的一个结构体,结构体中包含了文件的一系列属性。
但是!在操作系统接口调用层面上,操作系统只认文件描述符fd,所以FILE中一定封装了文件描述符fd。
4.3 fd
Q:文件描述符是什么,为什么进程通过一个文件描述符就能读写文件?
A:当我们调用一个进程的时候,进程是有可能打开多个文件的。而进程如何管理这些文件呢?“先描述,再组织”,所以进程就会在自己的PCB中维护一张struct files_struct表(由files指针指向),而这个表就叫做文件描述符表,在这个表中还会有一个struct file* fd_array[]的数组,是一个结构体指针数组,里面存放着我们打开的文件地址。所以:文件描述符fd实际上是进程PCB中,files指针指向的,结构体中的,数组的,下标。

小知识点:对文件内容做任何操作,都必须把文件加载到内核对应的文件缓冲区中。所谓的加载就是从磁盘到内存的拷贝。
5. 重定向原理
文件描述符的分配原则:当我们申请文件描述符时,操作系统遍历fd_array[]数组,找到一个最小的没有被使用的分配给fd。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{close(1);int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);printf("fd = %d", fd);return 0;
}
所以当我们代码中将标准输出关闭之后,又打开新文件,操作系统就会将1号下表分配给我们的新文件,然后我们又调用了printf函数,而printf函数只认stdout,其实认的是文件描述符fd == 1,printf就是向文件描述符为1的文件中写入内容,由此实现了重定向。本质上是更改文件描述符表的指针指向。于是我们引出了系统调用函数dup2
6. dup2
dup2 函数是用于复制文件描述符的系统调用,它可以将一个已有的文件描述符复制到另一个指定的文件描述符。


示例如下:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>int main() {// 打开文件(写入模式,不存在则创建,权限 0644)int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd == -1) {perror("open failed");return 1;}// 将标准输出(stdout,文件描述符 1)重定向到 fd 指向的文件if (dup2(fd, 1) == -1) {perror("dup2 failed");close(fd);return 1;}// 关闭原 fd(已通过 newfd=1 引用,无需保留)close(fd);// 此时 printf 会写入文件而非终端printf("这段文字会被写入 output.txt\n");return 0;
}
7. 总结
本文就完成了系统层面的文件操作,也让我更深刻的理解了语言函数的实现原理。
完
