今天学的东西,跟上一篇的fopen、fread、fwrite看着很像,但其实有本质区别。之前学的是C标准库函数,跨平台通用;今天学的是Linux系统调用,是操作系统内核直接提供的接口。简单说,fopen底层也是调用了open,但封装了缓冲区等细节,用起来更方便;而open、read、write更接近底层,粒度更细,控制也更灵活。
这篇就记录一下open、read、write、close这几个核心系统调用的用法,以及文件描述符这个概念。
open:打开文件,拿到“通行证”
系统调用里打开文件用的是open,跟fopen最大的区别是:它返回一个整数,叫文件描述符,而不是FILE指针。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int fd = open("test.txt", O_RDWR | O_CREAT, 0664);
第一个参数是路径,第二个是打开方式(flags),第三个是权限(mode),只在创建文件时才生效。
常用的flags有:
· O_RDONLY:只读
· O_WRONLY:只写
· O_RDWR:读写
· O_CREAT:文件不存在则创建
· O_TRUNC:打开时清空文件内容
· O_APPEND:写入时追加到末尾
这些flags可以用按位或|组合起来。比如O_WRONLY | O_CREAT | O_TRUNC就相当于标准库里的"w"模式——只写、不存在则创建、存在则清空。
第三个参数mode指定文件权限,比如0664表示文件所有者可读写、同组用户可读写、其他人只读。不过实际生效的权限还要减去系统的umask值,这是Linux的安全机制,防止创建权限过宽的文件。
open成功返回一个非负整数(文件描述符),失败返回-1。所以每次open之后都要判断一下fd == -1,不然拿到个无效描述符,后面的read、write全都会出错。
文件描述符:0、1、2的秘密
文件描述符本质上就是一个整数,操作系统用这个整数来代表一个已打开的文件。它的分配规则很简单:每次分配当前最小的未使用整数。
系统默认已经打开了三个:
· 0(STDIN_FILENO):标准输入,默认是键盘
· 1(STDOUT_FILENO):标准输出,默认是终端
· 2(STDERR_FILENO):标准错误,默认也是终端
这三个是每个程序启动时自动打开的,直接就能用。比如用write(1, "hello\n", 6)就能在终端打印"hello",效果和printf一样,只不过printf是标准库封装过的,更方便。
每个进程能打开的文件描述符数量是有限的,默认一般是1024个(0到1023),不过系统上限可以调整。所以用完文件一定记得关,否则会泄漏。
read和write:读写操作
read和write跟标准库里的fread、fwrite类似,但参数顺序和返回值略有不同。
write —— 写入数据
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数:文件描述符、数据缓冲区、要写的字节数。返回值是实际写入了多少字节,成功时等于count,失败返回-1。
注意,write不保证一次就能写完所有数据,尤其在网络编程或者磁盘繁忙的时候。所以通常要判断返回值,如果小于count,要循环写入剩余的部分。
read —— 读取数据
ssize_t read(int fd, void *buf, size_t count);
参数类似,从文件描述符fd中读取最多count字节到buf里。返回值是实际读到的字节数:如果大于0,说明读到了数据;如果等于0,说明读到了文件末尾;如果小于0,说明出错了。
read和write操作的是原始字节,没有换行、字符串之类的概念,也不像fgets那样自动帮你加\0,所以缓冲区要自己管理好,防止溢出。
close关掉它
int close(int fd);
close就是关闭文件描述符,释放资源。成功返回0,失败返回-1。
用了open就一定要配close,这点和标准库是一个道理。否则文件描述符用完了,程序后面就再也打不开新文件了。
一个完整的例子:拷贝文件
下面这段代码用系统调用实现一个简单的文件拷贝程序,把src.txt的内容复制到dst.txt:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int src = open("src.txt", O_RDONLY);
if (src == -1)
{
perror("打开源文件失败");
return 1;
}
int dst = open("dst.txt", O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (dst == -1)
{
perror("打开目标文件失败");
close(src);
return 1;
}
char buf[1024];
ssize_t n;
while ((n = read(src, buf, sizeof(buf))) > 0)
{
write(dst, buf, n); // 实际应该检查返回值,这里简写了
}
close(src);
close(dst);
return 0;
}
这个例子跟标准库版本的核心区别就是:没有缓冲区自动管理,每次read把数据读进自己定义的buf,再用write写出去。虽然代码看起来差不多,但系统调用少了用户态和内核态之间的一层封装,理论上更直接。
标准库 vs 系统调用,怎么选?
学到这里其实有个疑惑:既然标准库用起来更方便,为什么还要学系统调用?
我的理解是:
· 标准库函数(fopen/fread/fwrite)自带缓冲区,适合常规文件读写,代码可移植,效率也不差。
· 系统调用(open/read/write)更底层,适合需要精细控制的场景,比如网络编程、设备驱动开发,或者需要处理信号中断、非阻塞IO等情况。
另外,open的flags比标准库的模式字符串丰富很多,比如O_NONBLOCK(非阻塞)、O_SYNC(同步写入),这些在标准库层面很难直接控制。
今天的内容偏底层,代码量不大,但理解文件描述符和系统调用的工作方式,对后面学进程、管道、socket这些很有帮助。下一步准备继续看看lseek、dup、fcntl这些更进阶的系统调用,争取把文件IO这块吃透。