目录
一、引入
二、标志位
1、什么是标志位?
2、标志位传递示例
输出结果分析
关键点解释
三、文件描述符(File Descriptor)(先大概了解)
四、接口介绍:open()函数
1、命令查看
2、头文件
3、函数原型
4、参数说明
1. open的第一个参数pathname
2. open的第二个参数
1. 必需标志(必须指定且只能指定一个)
2. 可选标志(可组合使用)
补充说明
扩展:
3. open的第三个参数
基本权限位
特殊权限位
如何使用 mode_t?
1. 直接使用八进制数
2. 使用宏定义组合
3. 设置特殊权限
mode_t 的实际影响
5、返回值
五、接口介绍:close()函数
1、函数原型
2、参数说明
3、返回值
4、常见错误码(errno)
5、基本用法
6、深入理解close操作(了解)
7、注意事项(了解)
六、接口介绍:write()函数
1. write函数原型
2. 参数说明
3. 返回值
4、对文件进行写入操作示例
5. write函数的特点和注意事项
1. 部分写入
2. 阻塞与非阻塞
3. 原子性
4. 文件位置指针
七、接口介绍:read()函数
1、函数原型
2、参数说明
3、返回值
4、对文件进行读取操作示例
八、系统调用和库函数
一、引入
操作系统提供多种文件访问方式,包括C语言接口、C++接口以及其他语言接口,同时也具备底层系统调用接口,系统调用才是文件操作最底层的实现方式。相较于高级语言库函数,系统调用更接近底层硬件。实际上,各种语言的库函数都是对系统接口的封装实现。
无论是在Linux还是Windows平台运行C代码,C库函数都通过封装各自操作系统的系统调用接口来实现跨平台性。这种设计不仅保证了语言的通用性,也为二次开发提供了便利。
在学习系统文件I/O前,需要先掌握标志位的传递方法,这在系统文件I/O接口中会频繁使用:
二、标志位
1、什么是标志位?
标志位(flag)是一种编程中常用的技术,它使用二进制位来表示不同的状态或选项。每个标志位通常对应一个特定的含义,通过位运算可以单独设置、清除或检查这些标志位。
标志位的优点包括:
-
节省内存(多个布尔状态可以用一个整数的不同位表示)
-
可以方便地组合多个状态(通过位或运算)
-
可以高效地检查特定状态(通过位与运算)
2、标志位传递示例
#include <stdio.h>// 定义三个标志位,每个标志位对应一个不同的二进制位
#define ONE 0x01 // 0000 0001 (二进制)
#define TWO 0x02 // 0000 0010
#define THREE 0x04 // 0000 0100void func(int flags) {// 检查flags是否包含ONE标志if (flags & ONE) printf("flags has ONE!\n");// 检查flags是否包含TWO标志if (flags & TWO) printf("flags has TWO!\n");// 检查flags是否包含THREE标志if (flags & THREE) printf("flags has THREE!\n");printf("\n");
}int main() {func(ONE); // 只传递ONE标志func(THREE); // 只传递THREE标志func(ONE | TWO); // 传递ONE和TWO标志的组合func(ONE | THREE | TWO); // 传递所有三个标志的组合return 0;
}
输出结果分析
-
func(ONE);
输出:flags has ONE!(只有ONE标志被设置) -
func(THREE);
输出:flags has THREE!(只有THREE标志被设置) -
func(ONE | TWO);
输出:flags has ONE! flags has TWO!(ONE和TWO标志被设置)
-
func(ONE | THREE | TWO);
输出:flags has ONE! flags has TWO! flags has THREE!(所有三个标志都被设置)
关键点解释
-
flags & ONE
:这是一个位与运算,用于检查flags变量中是否设置了ONE标志位。如果结果为非零,则表示设置了该标志。 -
ONE | TWO
:这是一个位或运算,用于组合多个标志位。结果是一个同时包含ONE和TWO标志的值。 -
标志位的值选择:每个标志位对应一个不同的二进制位(0x01, 0x02, 0x04等),这样它们可以独立设置和检查而不会相互干扰。
这种标志位技术在系统编程、硬件接口和需要高效表示多个选项的场景中非常常见。
三、文件描述符(File Descriptor)(先大概了解)
在Unix/Linux系统中,所有I/O操作都是通过文件描述符完成的。文件描述符是一个非负整数,用于标识打开的文件。系统为每个进程维护一个文件描述符表。
三个标准的文件描述符:
-
0: 标准输入(stdin)
-
1: 标准输出(stdout)
-
2: 标准错误(stderr)
四、接口介绍:open()
函数
open()
函数是Linux/Unix系统中用于打开或创建文件的核心系统调用之一,它是文件操作的基础。
1、命令查看
man 2 open
2、头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
3、函数原型
系统接口中使用open函数打开文件,open函数的函数原型如下:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
4、参数说明
1. open的第一个参数pathname
open函数的第一个参数是pathname,表示要打开或创建的文件路径名,可以是相对路径或绝对路径。
- 若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
- 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)
2. open的第二个参数
open函数的第二个参数是flags(文件打开方式标志位),表示打开文件的标志,控制文件的打开方式和行为。flags参数由以下一个或多个值通过位或(|
)操作组合而成。
例如,若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置如下:
O_WRONLY | O_CREAT
1. 必需标志(必须指定且只能指定一个)
标志 | 说明 |
---|---|
O_RDONLY | 以只读方式打开文件 |
O_WRONLY | 以只写方式打开文件 |
O_RDWR | 以读写方式打开文件 |
2. 可选标志(可组合使用)
标志 | 说明 |
---|---|
O_CREAT | 如果文件不存在,则创建它(需配合 mode 参数设置权限) |
O_EXCL | 与 O_CREAT 一起使用,确保文件不存在时才创建(用于原子性创建文件) |
O_TRUNC | 如果文件已存在且是普通文件,则截断为0字节(清空文件) |
O_APPEND | 追加模式,每次写入都会自动追加到文件末尾(避免并发写入冲突) |
O_NONBLOCK / O_NDELAY | 非阻塞模式打开文件(适用于 FIFO、管道、设备文件等) |
O_SYNC | 同步 I/O,每次写操作都会等待数据真正写入物理存储(性能较低,但数据更安全) |
O_NOFOLLOW | 如果路径是符号链接,则不跟随(防止符号链接攻击) |
O_DIRECTORY | 如果路径不是目录,则打开失败(确保只打开目录) |
O_CLOEXEC | 设置 close-on-exec 标志,exec 时自动关闭文件描述符(防止子进程继承) |
补充说明
-
必需标志(
O_RDONLY
/O_WRONLY
/O_RDWR
)必须选且仅选一个。 -
可选标志可以通过
|
(按位或)组合使用,例如:int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
-
O_EXCL
必须与O_CREAT
一起使用,否则无意义。 -
O_TRUNC
仅对普通文件有效,对目录、设备文件等无效。 -
O_APPEND
在多进程/多线程写入时能避免竞争条件(Race Condition)。 -
O_SYNC
会影响性能,但能确保数据持久化(适用于关键数据存储)。
扩展:
系统接口open的第二个参数flags是整型,有32比特位,若将一个比特位作为一个标志位,则理论上flags可以传递32种不同的标志位。
实际上传入flags的每一个选项在系统当中都是以宏的方式进行定义的:
例如,O_RDONLY、O_WRONLY、O_RDWR和O_CREAT在系统当中的宏定义如下:
#define O_RDONLY 00
#define O_WRONLY 01
#define O_RDWR 02
#define O_CREAT 0100
这些宏定义选项的二进制编码具有一个共同特征:每个选项的二进制序列中仅有一位为1(O_RDONLY选项除外,其二进制值为全0,表示默认选项)。不同选项的置1位各不相同,这使得open函数内部可以通过简单的"与"运算来检测特定选项是否被设置。
int open(arg1, arg2, arg3)
{if (arg2&O_RDONLY)//检查是否设置了O_RDONLY选项{}if (arg2&O_WRONLY)//检查是否设置了O_WRONLY选项{}if (arg2&O_RDWR)//检查是否设置了O_RDWR选项{}if (arg2&O_CREAT)//检查是否设置了O_CREAT选项{}//...
}
3. open的第三个参数
在 Unix/Linux 系统调用中,mode_t
是一个数据类型,用于表示文件的权限模式(permission mode)。它通常是一个无符号整数类型(如 unsigned int
),用于指定文件的访问权限。
当使用O_CREAT
创建新文件时,必须指定mode参数,表示新文件的权限。mode通常用八进制表示,如0644。
例如,设置mode=0666
会赋予文件-rw-rw-rw-
的权限。
需要注意的是,实际文件权限会受到umask
(文件创建掩码)的影响。计算公式为:实际权限 = mode & (~umask)
。在默认umask=0002
的情况下,当mode=0666
时,实际创建的权限为0664
(即-rw-rw-r--
)。
若要完全按照mode
参数设置权限,可以在创建文件前调用umask(0)
将掩码清零。
umask(0); //将文件默认掩码设置为0
注意: 当不需要创建文件时,open的第三个参数可以不必设置。
open()
函数在创建文件(使用 O_CREAT
标志)时,需要指定文件的权限模式 mode_t
。这个参数决定了文件的读、写、执行权限,以及特殊权限位(如 setuid、setgid 等)。
基本权限位
mode_t
由多个权限位组合而成,可以使用八进制数或宏定义来设置:
宏定义 | 八进制值 | 权限说明 |
---|---|---|
S_IRUSR | 0400 | 用户(owner)可读 |
S_IWUSR | 0200 | 用户可写 |
S_IXUSR | 0100 | 用户可执行 |
S_IRGRP | 0040 | 组(group)可读 |
S_IWGRP | 0020 | 组可写 |
S_IXGRP | 0010 | 组可执行 |
S_IROTH | 0004 | 其他用户(others)可读 |
S_IWOTH | 0002 | 其他用户可写 |
S_IXOTH | 0001 | 其他用户可执行 |
特殊权限位
宏定义 | 八进制值 | 权限说明 |
---|---|---|
S_ISUID | 04000 | 设置用户ID(setuid) |
S_ISGID | 02000 | 设置组ID(setgid) |
S_ISVTX | 01000 | 粘滞位(sticky bit) |
如何使用 mode_t
?
1. 直接使用八进制数
最常见的用法是直接使用 3位八进制数 来设置权限:
int fd = open("example.txt", O_CREAT | O_WRONLY, 0644);
-
0644
表示:-
用户(owner):
6
(4+2
,即rw-
) -
组(group):
4
(r--
) -
其他用户(others):
4
(r--
)
-
2. 使用宏定义组合
也可以使用宏定义组合:
int fd = open("example.txt", O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
-
等同于
0644
(rw-r--r--
)
3. 设置特殊权限
例如,设置 setuid
权限(仅对可执行文件有效):
int fd = open("program", O_CREAT | O_WRONLY, S_IRWXU | S_ISUID);
-
S_IRWXU
=0700
(rwx------
) -
S_ISUID
=04000
(设置setuid
位) -
最终权限:
4700
(rws------
)
mode_t
的实际影响
-
open()
的mode
参数仅在O_CREAT
时生效(如果文件已存在,则不会修改权限)。 -
最终权限会受到
umask
的影响:mode_t final_mode = mode & ~umask;
例如,如果
umask=002
,而mode=0666
,则实际权限是0664
(rw-rw-r--
)。
5、返回值
open函数的返回值是新打开文件的文件描述符。
- 成功:成功时返回一个非负整数文件描述符。
- 失败:失败时返回-1并设置errno。
我们可以尝试一次打开多个文件,然后分别打印它们的文件描述符:
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{umask(0);int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);printf("fd1:%d\n", fd1);printf("fd2:%d\n", fd2);printf("fd3:%d\n", fd3);printf("fd4:%d\n", fd4);printf("fd5:%d\n", fd5);return 0;
}
运行程序后可以看到,打开文件的文件描述符是从3开始连续且递增的:
我们再尝试打开一个根本不存在的文件,也就是open函数打开文件失败:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd = open("test.txt", O_RDONLY);printf("%d\n", fd);return 0;
}
运行程序后可以看到,打开文件失败时获取到的文件描述符是-1:
文件描述符本质上是一个指针数组的索引,该数组中的每个指针都指向一个已打开文件的文件信息。通过文件描述符即可访问对应的文件信息。
当open函数成功打开文件时,系统会扩展指针数组并返回新增指针的索引值;若打开失败则直接返回-1。因此,连续成功打开多个文件时,获得的文件描述符是依次递增的。
Linux进程默认打开三个标准文件描述符:0(标准输入)、1(标准输出)和2(标准错误)。这就是新打开文件时,文件描述符从3开始分配的原因。
open
函数的具体使用方式取决于应用场景:
- 若目标文件不存在,需要创建新文件,则使用带三个参数的
open
(第三个参数表示创建文件的默认权限) - 若文件已存在,则使用带两个参数的
open
五、接口介绍:close()
函数
close()
函数是Linux/Unix系统中用于关闭已打开文件描述符的重要系统调用。
1、函数原型
#include <unistd.h>int close(int fd);
2、参数说明
fd(文件描述符)
:
要关闭的文件描述符(file descriptor),这是之前通过open()
、creat()
、pipe()
、dup()
等函数获得的文件描述符。
3、返回值
-
成功时返回0
-
失败时返回-1,并设置errno来指示错误原因
4、常见错误码(errno)
-
EBADF
:fd不是有效的已打开文件描述符 -
EINTR
:close操作被信号中断 -
EIO
:发生了I/O错误
5、基本用法
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>int main() {int fd = open("example.txt", O_RDONLY);if (fd == -1) {perror("open failed");return 1;}// 使用文件描述符进行读写操作...if (close(fd) == -1) {perror("close failed");return 1;}return 0;
}
6、深入理解close操作(了解)
-
资源释放:
-
关闭文件描述符会释放内核为该文件分配的所有资源
-
释放文件描述符本身,使其可被后续的
open()
或pipe()
等调用重用
-
-
缓冲区刷新:
-
对于输出文件,close操作会确保所有缓冲数据被写入磁盘
-
对于使用
mmap()
映射的文件,close操作不会解除映射,但关闭后访问映射内存可能导致SIGBUS信号
-
-
文件锁释放:
-
进程终止时所有文件描述符会自动关闭
-
关闭文件描述符会释放该进程在该文件上设置的所有锁(使用
fcntl()
设置的锁)
-
7、注意事项(了解)
-
多次关闭:
-
重复关闭同一个文件描述符是错误行为
-
在多线程环境中尤其需要注意,可能引发竞态条件
-
-
信号中断处理:
-
如果close()被信号中断,某些系统上需要重新调用close()
-
更安全的做法是使用以下模式:
while (close(fd) == -1) {if (errno != EINTR) {perror("close error");break;}// 如果是被信号中断,则继续尝试关闭 }
-
-
文件描述符泄漏:
-
忘记关闭文件描述符是常见编程错误
-
长期运行的进程可能导致文件描述符耗尽
-
建议在打开文件后立即考虑关闭操作,使用
goto
或RAII模式管理资源。
-
六、接口介绍:write()
函数
write()
是Linux/Unix系统中一个非常重要的低级文件I/O函数,用于将数据写入文件描述符对应的文件或设备。
1. write函数原型
#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
2. 参数说明
-
fd
:文件描述符,通常由open()
函数返回 -
buf
:指向要写入数据的缓冲区的指针 -
count
:要写入的字节数
3. 返回值
-
成功时:返回实际写入的字节数(可能小于请求的
count
) -
失败时:返回-1,并设置errno
4、对文件进行写入操作示例
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open");return 1;}const char* msg = "hello syscall\n";for (int i = 0; i < 5; i++){write(fd, msg, strlen(msg));}close(fd);return 0;
}
运行程序后,在当前路径下就会生成对应文件,文件当中就是我们写入的内容:
5. write函数的特点和注意事项
1. 部分写入
write()
可能会执行部分写入,即返回值小于请求的字节数。这种情况常见于:
-
磁盘空间不足
-
被信号中断
-
非阻塞模式下资源暂时不可用
2. 阻塞与非阻塞
-
常规文件通常不会阻塞
-
管道、套接字等特殊文件可能阻塞
-
可以设置
O_NONBLOCK
标志使操作非阻塞
3. 原子性
对于常规文件,小于PIPE_BUF
大小的写入是原子的(通常为4096字节)
4. 文件位置指针
write()
操作会更新文件的当前位置指针
七、接口介绍:read()函数
read()
函数是Linux/Unix系统中用于从文件描述符读取数据的基本系统调用之一。它是文件I/O操作的核心函数之一,属于POSIX标准的一部分。
1、函数原型
#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);
2、参数说明
-
fd (file descriptor):文件描述符,是一个整数值,指向要读取的文件
-
通常由
open()
函数返回 -
标准输入的文件描述符是0
-
-
buf:指向内存缓冲区的指针,用于存放读取到的数据。必须预先分配足够的内存空间
-
count:请求读取的字节数。通常是缓冲区的大小
3、返回值
-
成功时:返回实际读取的字节数
-
可能小于请求的字节数(例如接近文件末尾时)
-
返回0表示到达文件末尾(EOF)
-
-
失败时:返回-1,并设置errno
-
常见的errno值:
-
EAGAIN/EWOULDBLOCK:非阻塞I/O且无数据可读
-
EBADF:无效的文件描述符
-
EINTR:被信号中断
-
EIO:I/O错误
-
-
4、对文件进行读取操作示例
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd = open("log.txt", O_RDONLY);if (fd < 0){perror("open");return 1;}char ch;while (1){ssize_t s = read(fd, &ch, 1);if (s <= 0){break;}write(1, &ch, 1); //向文件描述符为1的文件写入数据,即向显示器写入数据}close(fd);return 0;
}
运行程序后,就会将我们刚才写入文件的内容读取出来,并打印在显示器上:
八、系统调用和库函数
总的来说,我们更加能明白开始时提到的“实际上,各种语言的库函数都是对系统接口的封装实现。”这句话!!!
在了解返回值之前,需要明确两个重要概念:系统调用和库函数
- fopen、fclose、fread、fwrite这些是C标准库提供的函数,称为库函数(libc)
- 而open、close、read、write、lseek等则是操作系统直接提供的接口,称为系统调用
- 这与我们之前讲解操作系统概念时展示的系统架构图是一致的
系统调用接口与库函数的关系十分清晰。可以明确地说,f#系列函数是对系统调用的封装,为二次开发提供了便利。