参考引用
- UNIX 环境高级编程 (第3版)
- 嵌入式Linux C应用编程-正点原子
#include
#include
#include
#include
#include
int main(void) {
char buffer[1024];
int fd1, fd2;
int ret;
/* 打开 src_file 文件 */
fd1 = open("./src_file", O_RDONLY);
if (-1 == fd1) {
printf("Error: open src_file failed!\n");
return -1;
}
/* 新建 dest_file 文件并打开 */
fd2 = open("./dest_file", O_WRONLY | O_CREAT | O_EXCL, S_IRWXU | S_IRGRP | S_IROTH);
if (-1 == fd2) {
printf("Error: open dest_file failed!\n");
ret = -1;
goto err1;
}
/* 将 src_file 文件读写位置移动到偏移文件头 500 个字节处 */
ret = lseek(fd1, 500, SEEK_SET);
if (-1 == ret)
goto err2;
/* 读取 src_file 文件数据,大小 1KByte */
ret = read(fd1, buffer, sizeof(buffer));
if (-1 == ret) {
printf("Error: read src_file filed!\n");
goto err2;
}
/* 将 dest_file 文件读写位置移动到文件头 */
ret = lseek(fd2, 0, SEEK_SET);
if (-1 == ret)
goto err2;
/* 将 buffer 中的数据写入 dest_file 文件,大小 1KByte */
ret = write(fd2, buffer, sizeof(buffer));
if (-1 == ret) {
printf("Error: write dest_file failed!\n");
goto err2;
}
printf("OK: test successful\n");
ret = 0;
err2:
close(fd2);
err1:
close(fd1);
return ret;
}
上述代码调用 open 函数会有一个返回值,如:fd1(源文件 src_file 被打开时所对应的文件描述符) 和 fd2(目标文件 dest_file 被打开时所对应的文件描述符),这是一个 int 类型的数据,在 open 函数执行成功的情况下,会返回一个非负整数,该返回值就是一个文件描述符(file descriptor)
一个进程可以打开多个文件,但可以打开的文件数是有限制,在 Linux 系统下,可以通过 ulimit 命令来查看进程可打开的最大文件数,最大值默认情况下是 1024
$ ulimit -n
1024
每一个硬件设备都对应于 Linux 下的某一个文件,把这类文件称为设备文件。所以设备文件对应的其实是某一硬件设备,应用程序通过对设备文件进行读写等操作,来使用、操控硬件设备,如:LCD 显示器、串口、音频、键盘等
$ man 2 open
OPEN(2) Linux Programmer's Manual OPEN(2)
NAME
open, openat, creat - open and possibly create a file
SYNOPSIS
#include
#include
#include
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
int fd = open("/home/dengtao/hello", O_RDWR | O_CREAT, S_IRWXU | S_IRGRP | S_IROTH);
if (-1 == fd)
return fd;
只有用户对该文件具有相应权限时,才可以使用对应的标志去打开文件,否则会打开失败
$ ls -il
total 84
# 每行第一个数字代表 inode 号,如 2494267
2494267 drwxrwxr-x 2 yue yue 4096 12月 10 18:23 mydir
2490502 -rwxrwxr-x 1 yue yue 8384 12月 10 18:08 open
2490536 -rwxrwxr-x 1 yue yue 8384 12月 10 18:09 open2
2494210 -rw-rw-r-- 1 yue yue 209 12月 10 18:09 open2.c
...
# 还可以使用 stat 命令查看 inode 号
$ stat test.c
stat open
File: open
Size: 8384 Blocks: 24 IO Block: 4096 regular file
Device: 801h/2049d Inode: 2490502 Links: 1
Access: (0775/-rwxrwxr-x) Uid: ( 1000/ yue) Gid: ( 1000/ yue)
Access: 2023-12-10 18:08:10.828318773 +0800
Modify: 2023-12-10 18:08:09.131470772 +0800
Change: 2023-12-10 18:26:07.894582897 +0800
Birth: -
打开一个文件,系统内部会将这个过程分为三步
- 系统找到这个文件名所对应的 inode 编号
- 通过 inode 编号从 inode table 中找到对应的 inode 结构体
- 根据 inode 结构体中记录的信息,确定文件数据所在的 block(块)位置,并读出数据
为什么需要内存中的动态文件?
- 因为磁盘、硬盘、U 盘等存储设备基本都是 Flash 块设备,而块设备硬件有读写限制等特征:块设备是以一块一块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节),一个字节的改动也需要将该字节所在的 block 全部读取出来进行修改,修改完成之后再写入块设备中,所以对块设备的读写操作非常不灵活
- 而内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常灵活,所以对于操作系统来说,会先将磁盘中的静态文件读取到内存中进行缓存,读写操作都是针对这份动态文件,而不是直接去操作磁盘中的静态文件,内存的读写速率远比磁盘读写快得多
// man 3 strerror 查看帮助手册
#include
char *strerror(int errnum);
#include
#include
#include
#include
#include
#include
#include
int main(void) {
int fd;
/* 打开文件,test_file 不存在 */
fd = open("./test_file", O_RDONLY);
if (-1 == fd) {
printf("Error: %s\n", strerror(errno));
return -1;
}
close(fd);
return 0;
}
$ gcc test.c -o test
$ ./test
Error: No such file or directory # strerror() 返回的字符串
// man 3 perror 查看帮助手册
#include
// s :在错误提示字符串信息之前,可加入自己的打印信息,不加则传入空字符串即可
void perror(const char *s);
#include
#include
#include
#include
#include
int main(void) {
int fd;
/* 打开文件,test_file 不存在 */
fd = open("./test_file", O_RDONLY);
if (-1 == fd) {
perror("open error");
return -1;
}
close(fd);
return 0;
}
$ gcc test.c -o test
$ ./test
open error: No such file or directory # open error 便是自己附加的打印信息
#include
void _exit(int status);
#include
void _Exit(int status);
...
fd = open("./test_file", O_RDONLY);
if (-1 == fd) {
perror("open error");
_exit(-1);
}
...
#include
void exit(int status);
空洞文件有什么用呢?
- 空洞文件对多线程共同操作文件是有用的,有时候创建一个很大的文件,如果单个线程从头开始依次构建该文件需要很长时间,有一种思路就是将文件分为多段,然后使用多线程来操作,每个线程负责其中一段数据的写入,最后将他们连接起来
如果 open() 函数带了 O_APPEND 标志,打开文件,当每次使用 write() 函数对文件进行写操作时,都会自动把文件当前位置偏移量移动到文件末尾,从文件末尾开始写入数据,也就是意味着每次写入数据都是从文件末尾开始
案例演示
#include
#include
#include
#include
int main(void) {
char buff[16];
int fd;
int ret;
/* 打开文件 */
fd = open("./test.txt", O_RDWR | O_APPEND);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 初始化 buffer 中的数据 */
memset(buff, 0x55, sizeof(buff));
/* 写入数据: 写入 4 个字节数据 */
ret = write(fd, buff, 4);
if (-1 == ret) {
perror("write error");
goto err;
}
/* 将 buffer 缓冲区中的数据全部清 0 */
memset(buff, 0x00, sizeof(buff));
/* 将位置偏移量移动到距离文件末尾 4 个字节处 */
ret = lseek(fd, -4, SEEK_END);
if (-1 == ret) {
perror("lseek error");
goto err;
}
/* 读取数据 */
ret = read(fd, buff, 4);
if (-1 == ret) {
perror("read error");
goto err;
}
printf("0x%x 0x%x 0x%x 0x%x\n", buff[0], buff[1], buff[2], buff[3]);
ret = 0;
err:
close(fd);
exit(ret);
}
$ gcc append.c -o append
$ ./append
0x55 0x55 0x55 0x55
#include
#include
#include
#include
#include
#include
int main(void) {
int fd1, fd2, fd3;
int ret;
/* 第一次打开文件 */
fd1 = open("./test.txt", O_RDWR);
if (-1 == fd1) {
perror("open error");
exit(-1);
}
/* 第二次打开文件 */
fd2 = open("./test.txt", O_RDWR);
if (-1 == fd2) {
perror("open error");
ret = -1;
goto err1;
}
/* 第三次打开文件 */
fd3 = open("./test.txt", O_RDWR);
if (-1 == fd3) {
perror("open error");
ret = -1;
goto err2;
}
/* 打印出 3 个文件描述符 */
printf("%d %d %d\n", fd1, fd2, fd3);
close(fd3);
ret = 0;
err2:
close(fd2);
err1:
close(fd1);
exit(ret);
}
$ gcc openmul.c -o openmul
$ ./openmul
3 4 5 # 得到相同数量的文件描述符(程序中分配得到的最小文件描述符一般是 3)
如果同一个文件被多次打开,那么该文件所对应的动态文件是否在内存中也存在多份?也就是说,多次打开同一个文件是否会将其文件数据多次拷贝到内存中进行维护? 答:不会
- 假如,内存中只有一份动态文件,那么读取得到的数据应该就是 0x11、0x22、0x33、0x44
- 如果存在多份动态文件,那么通过 fd2 读取的是与它对应的动态文件中的数据,那就不是 0x11、0x22、0x33、0x44,而是读取出 0 个字节数据,因为它的文件大小是 0
#include
#include
#include
#include
#include
#include
#include
int main(void) {
char buffer[4];
int fd1, fd2;
int ret;
/* 创建新文件 test_file 并打开 */
fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (-1 == fd1) {
perror("open error");
exit(-1);
}
/* 再次打开 test_file 文件 */
fd2 = open("./test_file", O_RDWR);
if (-1 == fd2) {
perror("open error");
ret = -1;
goto err1;
}
/* 通过文件描述符 fd1 写入 4 个字节数据 */
buffer[0] = 0x11;
buffer[1] = 0x22;
buffer[2] = 0x33;
buffer[3] = 0x44;
ret = write(fd1, buffer, 4);
if (-1 == ret) {
perror("write error");
goto err2;
}
/* 将读写位置偏移量移动到文件头 */
ret = lseek(fd2, 0, SEEK_SET);
if (-1 == ret) {
perror("lseek error");
goto err2;
}
/* 通过文件描述符 fd2 读取数据 */
memset(buffer, 0x00, sizeof(buffer));
ret = read(fd2, buffer, 4);
if (-1 == ret) {
perror("read error");
goto err2;
}
printf("0x%x 0x%x 0x%x 0x%x\n", buffer[0], buffer[1], buffer[2], buffer[3]);
ret = 0;
err2:
close(fd2);
err1:
/* 关闭文件 */
close(fd1);
exit(ret);
}
多个不同的进程中调用 open() 打开磁盘中的同一个文件,同样在内存中也只是维护了一份动态文件,多个进程间共享,它们有各自独立的文件读写位置偏移量
一个进程中两次调用 open 函数打开同一个文件,分别得到两个文件描述符 fd1 和 fd2,使用这两个文件描述符对文件进行写入操作,那么它们是分别写(各从各的位置偏移量开始写)还是接续写(一个写完,另一个接着后面写)?
#include
#include
#include
#include
#include
#include
#include
int main(void) {
unsigned char buffer1[4], buffer2[4];
int fd1, fd2;
int ret;
int i;
/* 创建新文件 test_file 并打开 */
fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (-1 == fd1) {
perror("open error");
exit(-1);
}
/* 再次打开 test_file 文件 */
fd2 = open("./test_file", O_RDWR);
if (-1 == fd2) {
perror("open error");
ret = -1;
goto err1;
}
/* buffer 数据初始化 */
buffer1[0] = 0x11;
buffer1[1] = 0x22;
buffer1[2] = 0x33;
buffer1[3] = 0x44;
buffer2[0] = 0xAA;
buffer2[1] = 0xBB;
buffer2[2] = 0xCC;
buffer2[3] = 0xDD;
/* 循环写入数据 */
for (i = 0; i < 4; i++) {
ret = write(fd1, buffer1, sizeof(buffer1));
if (-1 == ret) {
perror("write error");
goto err2;
}
ret = write(fd2, buffer2, sizeof(buffer2));
if (-1 == ret) {
perror("write error");
goto err2;
}
}
/* 将读写位置偏移量移动到文件头 */
ret = lseek(fd1, 0, SEEK_SET);
if (-1 == ret) {
perror("lseek error");
goto err2;
}
/* 读取数据 */
for (i = 0; i < 8; i++) {
ret = read(fd1, buffer1, sizeof(buffer1));
if (-1 == ret) {
perror("read error");
goto err2;
}
printf("%x%x%x%x", buffer1[0], buffer1[1],
buffer1[2], buffer1[3]);
}
printf("\n");
ret = 0;
err2:
close(fd2);
err1:
/* 关闭文件 */
close(fd1);
exit(ret);
}
$ gcc open.c -o open
$ ./open
# 这两个文件描述符对文件是分别写,通过 fd1 写入的数据被 fd2 写入的数据给覆盖了
aabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccddaabbccdd
将分别写更改为接续写
// 修改上述代码的下两行,添加 O_APPEND 标志即可
fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL | O_APPEND, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
fd2 = open("./test_file", O_RDWR | O_APPEND);
$ gcc open.c -o open
$ ./open
11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd
Linux 系统中,open 返回得到的文件描述符 fd 可以进行复制,复制成功之后可以得到一个新的文件描述符
复制得到的文件描述符与旧的文件描述符都指向了同一个文件表
#include
int dup(int oldfd); // oldfd :需要被复制的文件描述符
// 其余代码同 9.3 案例 3
...
/* 复制文件描述符 fd1 得到新的文件描述符 fd2 */
fd2 = dup(fd1);
if (-1 == fd2) {
perror("dup error");
ret = -1;
goto err1;
}
printf("fd1: %d\nfd2: %d\n", fd1, fd2);
...
$ gcc test.c -o test
$ ./test
fd1: 3 # 程序中分配得到的最小文件描述符一般是 3
fd2: 4
11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd
dup 分配的文件描述符是由系统分配的,遵循文件描述符分配原则,并不能自己指定一个文件描述符,这是 dup 系统调用的缺陷;而 dup2 系统调用修复了这个缺陷,可以手动指定文件描述符,而不需要遵循文件描述符分配原则
#include
// oldfd :需要被复制的文件描述符
// newfd :指定一个文件描述符(需要指定一个当前进程没有使用到的文件描述符)
int dup2(int oldfd, int newfd);
测试案例
#include
#include
#include
#include
#include
#include
int main(void) {
int fd1, fd2;
int ret;
/* 创建新文件 test_file 并打开 */
fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (fd1 == -1) {
perror("open error");
exit(-1);
}
fd2 = dup2(fd1, 100); // 使用 dup2 函数复制文件描述符 fd1,指定新的文件描述符为 100
if (fd2 == -1) {
perror("dup error");
ret = -1;
goto err1;
}
printf("fd1: %d\nfd2: %d\n", fd1, fd2);
ret = 0;
close(fd2);
err1:
close(fd1);
exit(ret);
}
$ gcc tmp2.c -o tmp2
$ ./tmp2
fd1: 3
fd2: 100
例:同一个文件对应两个不同的文件描述符 fd1 和 fd2,当使用 fd1 对文件进行写操作之后,并没有关闭 fd1,而此时使用 fd2 对文件再进行写操作,这其实就是一种文件共享
1. 同一个进程中多次调用 open 函数打开同一个文件
2. 不同进程中分别使用 open 函数打开同一个文件
如何规避或消除竞争状态?
- 问题所在:上述的问题出在逻辑操作 “先定位到文件末尾,然后再写”,它使用了两个分开的函数调用,首先使用 lseek 函数将文件当前位置偏移量移动到文件末尾,然后再使用 write 函数将数据写入到文件
- 解决办法:将这两个操作步骤合并成一个原子操作,所谓原子操作,是由多步操作组成的一个操作,原子操作要么一步也不执行,一旦执行,必须要执行完所有步骤,不可能只执行所有步骤中的一个子集(即 lseek 和 write 要么同时执行完,要么一个都不执行)
1. O_APPEND 实现原子操作
2. pread() 和 pwrite()
#include
// offset:表示当前需要进行读或写的位置偏移量
// 其余参数和返回值同 read、write
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
虽然 pread(或 pwrite)函数相当于 lseek 与 read(或 write)函数的集合,但还是有下列区别
- 1、调用 pread 函数时,无法中断其定位和读操作(也就是原子操作)
- 2、不更新文件表中的当前位置偏移量
#include
#include
#include
#include
#include
#include
int main(void) {
unsigned char buffer[100];
int fd;
int ret;
/* 打开文件 test_file */
fd = open("./test_file", O_RDWR);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 使用 pread 函数读取 100 字节数据(从偏移文件头 1024 字节处开始读取) */
ret = pread(fd, buffer, sizeof(buffer), 1024);
if (-1 == ret) {
perror("pread error");
goto err;
}
/* 获取当前位置偏移量 */
ret = lseek(fd, 0, SEEK_CUR);
if (-1 == ret) {
perror("lseek error");
goto err;
}
printf("Current Offset: %d\n", ret);
ret = 0;
err:
/* 关闭文件 */
close(fd);
exit(ret);
}
$ gcc pread.c -o pread
$ ./pread
# 假如 pread 函数会改变文件表中记录的当前位置偏移量,则打印出来的数据应该是 1024 + 100 = 1124
# 如果 pread 函数不改变文件表中记录的当前位置偏移量,则打印出来的数据应该是 0
# 如果把 pread 换成 read,那么打印出来的数据就是 100,因为读取了 100 个字节数据,相应的当前位置偏移量会向后移动 100
Current Offset: 0
#include
#include
// fd:文件描述符
// cmd:操作命令
// …:fcntl 函数是一个可变参函数,第三个参数需要根据不同的 cmd 来传入对应的实参,配合 cmd 来使用
int fcntl(int fd, int cmd, ... /* arg */ )
cmd 操作命令大致可以分为以下 5 种功能
- 复制文件描述符(cmd=F_DUPFD 或 cmd=F_DUPFD_CLOEXEC)
- 获取/设置文件描述符标志(cmd=F_GETFD 或 cmd=F_SETFD)
- 获取/设置文件状态标志(cmd=F_GETFL 或 cmd=F_SETFL)
- 获取/设置异步 IO 所有权(cmd=F_GETOWN 或 cmd=F_SETOWN)
- 获取/设置记录锁(cmd=F_GETLK 或 cmd=F_SETLK)
1. 复制文件描述符
#include
#include
#include
#include
#include
#include
int main(void) {
int fd1, fd2;
int ret;
fd1 = open("./test_file", O_RDONLY);
if (-1 == fd1) {
perror("open error");
exit(-1);
}
// 0 表示指定复制得到的新文件描述符必须要大于或等于 0
// 如果传入的第三个参数是 100,那么 fd2 就会等于 100
fd2 = fcntl(fd1, F_DUPFD, 0);
if (-1 == fd1) {
perror("fcntl error");
ret = -1;
goto err;
}
printf("fd1: %d\nfd2: %d\n", fd1, fd2);
ret = 0;
close(fd2);
err:
close(fd1);
exit(ret);
}
$ gcc fcntl.c -o fcntl
$ ./fcntl
fd1: 3
fd2: 4
2. 获取/设置文件状态标志
#include
#include
#include
#include
#include
#include
int main(void) {
int fd;
int ret;
int flag;
/* 打开文件 test_file */
fd = open("./test_file", O_RDWR);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 获取文件状态标志并打印出来 */
flag = fcntl(fd, F_GETFL);
if (-1 == flag) {
perror("fcntl F_GETFL error");
ret = -1;
goto err;
}
printf("flags: 0x%x\n", flag);
/* 设置文件状态标志,在原标志的基础上添加 O_APPEND 标志 */
// 第三个参数表示需要设置的文件状态标志,
ret = fcntl(fd, F_SETFL, flag | O_APPEND);
if (-1 == ret) {
perror("fcntl F_SETFL error");
goto err;
}
ret = 0;
err:
/* 关闭文件 */
close(fd);
exit(ret);
}
$ gcc fcntl2.c -o fcntl2
$ ./fcntl2
flags: 0x8002
#include
#include
// ftruncate() 使用文件描述符 fd 来指定目标文件
// truncate() 直接使用文件路径 path 来指定目标文件
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
#include
#include
#include
#include
#include
#include
int main(void) {
int fd;
/* 打开 file1 文件 */
if ((fd = open("./file1", O_RDWR)) < 0) {
perror("open error");
exit(-1);
}
/* 使用 ftruncate 将 file1 文件截断为长度 0 字节 */
if (ftruncate(fd, 0) < 0) {
perror("ftruncate error");
exit(-1);
}
/* 使用 truncate 将 file2 文件截断为长度 1024 字节 */
if (truncate("./file2", 1024) < 0) {
perror("truncate error");
exit(-1);
}
/* 关闭 file1 退出程序 */
close(fd);
exit(0);
}
$ ls -l
total 12
-rw-rw-r-- 1 yxd yxd 13 12月 14 15:50 file1
-rw-rw-r-- 1 yxd yxd 13 12月 14 15:51 file2
-rw-rw-r-- 1 yxd yxd 414 12月 14 15:49 trunc.c
$ gcc trunc.c -o trunc
$ ./trunc
$ ls -l
total 20
-rw-rw-r-- 1 yxd yxd 0 12月 14 15:52 file1
-rw-rw-r-- 1 yxd yxd 1024 12月 14 15:52 file2
-rwxrwxr-x 1 yxd yxd 8504 12月 14 15:52 trunc
-rw-rw-r-- 1 yxd yxd 414 12月 14 15:49 trunc.c