前言:本篇主要讲解底层文件系统接口,详细介绍 open 接口和它的 flags 参数 (即系统传递标记位),重点讲解 O_RDWR, O_RDONLY, O_WRONLY, O_CREAT 和 O_APPEND 这些操作模式。
一、先来段代码回顾C文件接口
hello.c写文件
#include <stdio.h>
#include <string.h>
int main()
{FILE *fp = fopen("myfile", "w");if(!fp){printf("fopen error!\n");}const char *msg = "hello bit!\n";int count = 5;while(count--){fwrite(msg, strlen(msg), 1, fp);}fclose(fp);return 0;
}
hello.c读文件
#include <stdio.h>
#include <string.h>
int main()
{FILE *fp = fopen("myfile", "r");if(!fp){printf("fopen error!\n");}char buf[1024];const char *msg = "hello bit!\n";while(1){//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明ssize_t s = fread(buf, 1, strlen(msg), fp);if(s > 0){buf[s] = 0;printf("%s", buf);}if(feof(fp)){break;}}fclose(fp);return 0;
}
输出信息到显示器,你有哪些方法
#include <stdio.h>
#include <string.h>
int main()
{const char *msg = "hello fwrite\n";fwrite(msg, strlen(msg), 1, stdout);printf("hello printf\n");fprintf(stdout, "hello fprintf\n");return 0;
}
1、 关于文件操作的思考
我们曾经讲过:文件 = 文件内容 + 文件属性 ①
文件属性也是数据!这意味着,即便你创建一个空文件,也要占据磁盘空间!所以:
② 文件操作 = 文件内容的操作 + 文件属性的操作
因此,在操作文件的过程中,既改变内容又改变属性的情况很正常,不要把它们割裂开来!
那么,所谓的 "打开" 文件,究竟在做什么? ③
"打开文件不是目的,访问文件才是目的!"
访问文件时,都是要通过 fread,fwrite,fgets... 这样的代码来完成对文件的操作的,
如果通过这些方式,那么 "打开" 文件就需要 将文件的属性或内容加载到内存 (memory) 中!
因为这是由冯诺依曼体系结构决定的,将来 \textrm{CPU} 要执行 fread,fwrite 来对文件进行读写的。
既然如此…… 是不是所有的文件都会处于被打开的状态呢?并不是! ④
那没有被打开的文件在哪里?
对于文件的理解,在宏观上我们可以区分成 内存文件 (打开的文件) 和 磁盘文件。 ⑤ (存储在磁盘中)
⑥ 通常我们打开文件、访问文件和关闭文件,是谁在进行相关操作?
运行起来的时候,才会执行对应的代码,然后才是真正的对文件进行相关的操作。
实际上是 进程在对文件进行操作! 在系统角度理解是我们曾经写的代码变成了进程。
进程执行调度对应的代码到了 fopen,write 这样的接口,然后才完成了对文件的操作。
当我执行 fopen 时,对应地就把文件打开了,所以文件操作和进程之间是撇不开关系。
🔺 结论:学习文件操作,实际上就是学习 "进程" 与 "打开文件" 的关系。 ⑦
2、当前路径(Current Path)
文件的本质实际上是进程与打开文件之间的关系。
因此文件操作和进程有关系,我们写一下我们的代码,获取进程 ,查一下进程信息:
#include <stdio.h>
#include <unistd.h>int main(void)
{FILE* pf = fopen("log.txt", "w"); // 写入if (pf == NULL) {perror("fopen");return 1;}/* 获取进程 pid */printf("Mypid: %d\n", getpid());/* 打开文件后,等一等,方便查询 */while (1) {sleep(1);}const char* msg = "hello!";int count = 1;while (count <= 10) {fprintf(pf, "%s: %d\n", msg, count++);}fclose(pf);
}
getpid 拿到进程 后,得益于 "昏睡指令" while(1){sleep(1);)
我们的进程就一直的跑着,再打开一个窗口,通过 $ls proc 指令检视该进程信息:
我们重点关注 和 , 后面链接指向的是可执行程序 mytest,即 路径 + 程序名。
而 (current working directory),即 当前工作目录,记录着当前进程所处的路径!
每个进程都有一个工作路径,所以我们上一节实现的简单 程序可以用 chdir 更改路径。
创建文件时,如果文件在当前目录下不存在,fopen 会默认在当前路径下自动创建文件。
默认创建在当前路径,和源代码、可执行程序在同一个路径下,因为这取决于 :
cwd -> /home/ayf/lesson1
所以,当前路径更准确的说法应该是:在当前进程所处的工作路径。
" 当前路径指的是在当前进程所处的工作路径 "
只不过默认情况下 (默认路径) ,一个进程的工作路径在它当前所处的路径而已,这是可以改的。
所以我们在写文件操作代码时,不带路径默认是源代码所在的路径,注意是默认!而已!
3、文件操作模式(File Operation Mode)
由文件操作符 (mode) 参数来指定,常用的模式包括:
man fopen
r:只读模式,打开一个已存在的文本文件,允许读取文件。r+:读写模式,打开一个已存在的文本文件,允许读写文件。w:只写模式,打开一个文本文件并清除其内容,如果文件不存在,则创建一个新文件。w+:读写模式,打开一个文本文件并清除其内容,如果文件不存在,则创建一个新文件。a:追加模式,打开一个文本文件并将数据追加到文件末尾,如果文件不存在,则创建一个新文件。a+:读写模式,打开一个文本文件并将数据追加到文件末尾,如果文件不存在,则创建一个新文件。
这里我们重点讲一下 a 和 a+
a 对应的是 appending 的首字母,意为 "追加" 。属于写入操作,不会覆盖源文件内容。
代码演示:测试追加效果
每次运行都会在 test.txt 里追加,我们多试几次看看:
a(append) 追加写入,可以不断地将文件中新增内容。(这让我联想到了追加重定向)
不同于 w,当我们以 w 方式打开文件准备写入的时候,其实文件已经先被清空了。
4、文件的读取(File Read)
我们复习一下文本行输入函数 fgets,它可以从特定的文件流中,以行为单位读取特定的数据:
char* fgets(char* s, int size, FILE* stream);_
代码演示:fgets()
#include <stdio.h>int main(void)
{FILE* pf = fopen("log.txt", "r"); // 读if (pf == NULL) {perror("fopen");//显示错误信息return 1;}char buffer[64]; // 用来存储while (fgets(buffer, sizeof(buffer), pf) != NULL) {// 打印读取到的内容printf("echo: %s", buffer);}fclose(pf);
}
运行结果如下:
我们下面再来实现一个类似 $cat 的功能,输入文件名打印对应文件内容。
代码演示:实现一个自己的 cat
#include <stdio.h>/* 读什么就打什么 mycat */
int main(int argc, char* argv[])
{if (argc != 2) {printf("Usage: %s filename\n", argv[0]);return 1;}FILE* pf = fopen(argv[1], "r"); // 读取if (pf == NULL) {perror("fopen");return 1;}char buffer[64];while (fgets(buffer, sizeof(buffer), pf) != NULL) {printf("%s", buffer);}fclose(pf);
}
读到什么就打印身边。
如果再把可执行程序 mytest 改名成 cat,$mv mytest cat
,
我们就实现了一个自己的 cat 代码。
二、文件系统接口(Basic File IO)
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问, 先来直接以代码的形式,实现和上面一模一样的代码:
写文件:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{umask(0);int fd = open("myfile", O_WRONLY|O_CREAT, 0644);if(fd < 0){perror("open");return 1;}int count = 5;const char *msg = "hello world!\n";int len = strlen(msg);while(count--){write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数
据。 返回值:实际写了多少字节数据}close(fd);return 0;
}
读文件:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}const char *msg = "hello world!\n";char buf[1024];while(1){ssize_t s = read(fd, buf, strlen(msg));//类比writeif(s > 0){printf("%s", buf);}else{break;}}close(fd);return 0;
}
1、系统调用与封装(Syscall and Wrapper)
当我们向文件写入时,最终是不是向磁盘写入?是!磁盘是硬件吗?就是硬件。
当我们像文件写入时,最后是向磁盘写入。磁盘是硬件,那么谁有资格向磁盘写入呢?
只能是操作系统!
既然是操作系统在写入,那我们自然不能绕开操作系统对磁盘硬件进行访问。
因为操作系统是对软硬件资源进行管理的大手子,你的任何操作都不能越过操作系统!
所有的上层访问文件的操作,都必须贯穿操作系统。
想要被上层使用,必须使用操作系统的相关的 系统调用 (syscall) !
回顾:: 如何理解 printf?我们怎么从来没有见过这些系统调用接口呢?
显示器是硬件,我们 printf 的消息打印到了硬件上,是你自己调用的 printf 打印到硬件上的,
但并不是你的程序显示到了显示器上,因为显示器是硬件,它的管理者只能是操作系统,
你不能绕过操作系统,而必须使用对应的接口来访问显示器,我们看到的 printf 一打,
内容就出现在屏幕上,实际上在函数的内部,一定是 调用了系统调用接口 的。
结论:printf 函数内部一定封装了系统调用接口。
所有的语言提供的接口,之所以你没有见到系统调用,因为所有的语言都被系统接口做了 封装。
所以你看不到对应的底层的系统接口的差别。为什么要封装?原生系统接口,使用成本比较高。
系统接口是 OS 提供的,就会带来一个问题:如果使用原生接口,你的代码只能在一个平台上跑。
直接使用原生系统接口,必然导致语言不具备 跨平台性 (Cross-platform) !
如果语言直接使用操作系统接口,那么它就不具备跨平台性,可是为什么采用封装就能解决?
封装是如何解决跨平台问题的呢?很简单:
" 穷举所有的底层接口 + 条件编译 "
结论:我们学习的接口,C 库提供的文件访问接口,系统调用。它们两具有上下层关系,C 底层一定会调用这些系统调用接口。
- 解释:不同的语言,有不同的文件访问接口。
- 系统调用:这就是我什么我们必须学习文件级别的系统接口!
2、文件打开:open()
打开文件,在 C 语言上是 fopen,在系统层面上是 open。
open 接口是我们要学习的系统接口中最重要的一个,没有之一!所以我们放到前面来讲。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int open(const char* pathname, int flags);
int open(const char* pathname, int flags, mode_t mode);
我们看到,这个 open 接口一个是两参数的,一个是三参数的,这个我们放到后面解释。
- open 接口的 pathname 参数表示要打开的文件名,和 C 语言的 fopen 一样,是要带路径的。
- flags 参数是打开文件要传递的选项,即 系统传递标记位,我们下面会重点讲解。
- mode 参数,就是 "文件操作模式" 了。
#if (defined _CRT_DECLARE_NONSTDC_NAMES && _CRT_DECLARE_NONSTDC_NAMES) || (!defined _CRT_DECLARE_NONSTDC_NAMES && !__STDC__)#define O_RDONLY _O_RDONLY#define O_WRONLY _O_WRONLY#define O_RDWR _O_RDWR#define O_APPEND _O_APPEND#define O_CREAT _O_CREAT#define O_TRUNC _O_TRUNC#define O_EXCL _O_EXCL#define O_TEXT _O_TEXT#define O_BINARY _O_BINARY#define O_RAW _O_BINARY#define O_TEMPORARY _O_TEMPORARY#define O_NOINHERIT _O_NOINHERIT#define O_SEQUENTIAL _O_SEQUENTIAL#define O_RANDOM _O_RANDOM
#endif
思考:在 Linux 下,C 语言中文件不存在,就直接创建它,创建是不是需要权限?
当然是需要的,我们需要给文件设置初始权限,这个 mode 参数就是干这个活的。
我们再来看看这个接口的返回值,居然是个 int,而不是我们 fopen 的 FILE*
- open 的返回值是个 int,返回 -1 表示 error,并设置 errno。
3、flags 系统传递标记位
我们可以输入 man 2 open 看看如何设置 flags 参数,实际上就是设置文件操作模式的。
我们重点关注下面这几个文件操作模式,它们被定义在 <fcntl.h> 头文件中:
O 实际上就是 Open 的意思,它们的用途通过名字不难猜:
- O_RDONLY: 只读打开
- O_WRONLY: 只写打开
- O_RDWR : 读,写打开 这三个常量,必须指定一个且只能指定一个
- O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
- O_APPEND: 追加写
返回值:
- 成功:新打开的文件描述符
- 失败:-1
int open(const char* pathname, int flags);
我们称 flags 为 标记位,并且它是个整数类型(C99 标准之前没有 bool 类型)
标记位实际上我们造就用过了,比如定义 flag 变量,设 flag=0,设 flag=1,传的都是单个的。
思考:但如果我想一次传递多个标志位呢?定义多个标记位?flag1, flag2, flag3...
方案:系统传递标记位是通过 位图 来进行传递的。
想必大家已经对位图不陌生了,在前几章我们讲解 waitpid 的 status 参数时就介绍过了:
status 参数也是整型,也是被当作一个 "位图结构" 看待的,这里的 flags 也是如此!
当成位图,就是一串整数。
我们可以让不同的位表示,是否只读,是否只写,是否读写…… 等等等等:
每个 宏标记一般只需要满足有一个比特位是 1,并且和其它宏对应的值不重叠 即可。
代码演示:我们创建一个 test.c
#include <stdio.h>#define PRINT_A 0x1 // 0000 0001
#define PRINT_B 0x2 // 0000 0010
#define PRINT_C 0x4 // 0000 0100
#define PRINT_D 0x8 // 0000 1000
#define PRINT_DFL 0x0// open
void Show (int flags /* 传递标志位 */)
{if (flags & PRINT_A) printf("Hello A\n");if (flags & PRINT_B) printf("Hello B\n");if (flags & PRINT_C) printf("Hello C\n");if (flags & PRINT_D) printf("Hello D\n");if (flags == PRINT_DFL) printf("Hello Default\n");
}int main(void)
{/* 我想打谁,只需要传对应的标记位即可 */printf("# PRINT_DFL: \n");Show(PRINT_DFL);printf("# PRINT_A: \n");Show(PRINT_A);printf("# PRINT_B: \n");Show(PRINT_B);printf("# PRINT_A AND PRINT_B: \n");Show(PRINT_A | PRINT_B);printf("# PRINT_C AND PRINT_D: \n");Show(PRINT_C | PRINT_D);printf("# PRINT_A AND PRINT_B AND PRINT_C AND PRINT_D: \n");Show(PRINT_A | PRINT_B | PRINT_C | PRINT_D);return 0;
}
4、open 接口用法演示
讲完了 flags 标记位,现在我们可以演示 open 接口的用法了。
int open(const char* pathname, int flags);
代码演示:是用 open() 打开 log.txt 文件没有就创建。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main(void)
{int fd = open("log.txt", O_WRONLY | O_CREAT);if (fd < 0) { // 打开失败perror("open"); return 1;}printf("fd: %d\n", fd); // 把 fd 打出来看看return 0;
}
此时,我们的log.txt是原本就存在的,
如果你要创建这个文件,该文件是要受到Linux 权限的约束的!
创建一个文件,你需要告诉操作系统默认权限是什么。
当我们要打开一个曾经不存在的文件,不能使用两个参数的 open,而要使用三个参数的 open!
也就是带 mode_t mode 的 open,这里的 mode 代表创建文件的权限:
int open(const char* pathname, int flags, mode_t mode);
代码演示:
int main()
{int fd = open("log.txt", O_APPEND | O_CREAT, 0666); // 八进制表示if (fd < 0) {perror("open");return 1;}printf("fd: %d\n", fd);return 0;
}
因为你要创建的文件,所以要听操作系统!我们来看看 umask:
你要 666,操作系统不能给你,因为 umask 是 0002,所以最多只能给你 664。
我们现在就是要 666,我们只需要调用 umask(),然后传 0:umask(0)
就可以让权限掩码暂时不听按操作系统的默认权限掩码,而用你设置的!
此时权限就变成了我们的666.
实际上,umask 命令就是调用这个接口的。
umask 设为 0,可以让我们以确定的权限打开文件,比如服务器要打开一个日志文件,权限就必须要按照它对应的权限设置好,不要采用系统的默认权限,可能会出问题。
5、文件关闭:close()
在 C 语言中,关闭文件可以调用 fclose,在系统接口中我们可以调用 close 来关闭:
#include <unistd.h>int close(int fd);
该接口相对 open 相对来说比较简单,只有一个 fd 参数,我们直接看代码:
1 #include <stdio.h> 2 #include <sys/types.h> 3 #include <sys/stat.h> 4 #include <fcntl.h> 5 #include <unistd.h> // 需引入头文件 6 int main() 7 { 8 umask(0); // umask现在就为0,听我的,别听操作系统的umask了 9 int fd = open("log.txt", O_WRONLY | O_CREAT, 0666); // 八进制表示 10 if (fd < 0) { 11 perror("open"); 12 return 1; 13 } 14 15 printf("fd: %d\n", fd); 16 close(fd);//关闭文件 17 18 return 0; 19 }
没啥问题,就。。。。。没了。
6、文件写入:write()
文件打开和文件关闭都有了,我们总要干点事吧?现在我们来做文件写入!
在 C 语言中我们用的是 fprintf, fputs, fwrite 等接口,而在系统中,我们可以调用 write 接口:
#include <unistd.h>
ssize_t write(int fd, const void* buf, size_t count);
write 接口有三个参数:
- fd:文件描述符
- buf:要写入的缓冲区的起始地址(如果是字符串,那么就是字符串的起始地址)
- count:要写入的缓冲区的大小
代码演示:向文件写入 6行信息
1 #include <stdio.h> 2 #include<string.h>3 #include <sys/types.h>4 #include <sys/stat.h>5 #include <fcntl.h>6 #include <unistd.h> // 需引入头文件 7 int main() 8 { 9 umask(0); // umask现在就为0,听我的,别听操作系统的umask了 10 int fd = open("log.txt", O_WRONLY | O_CREAT, 0666); // 八进制表示 11 if (fd < 0) { 12 perror("open"); 13 return 1; 14 } 15 int cnt=6; 16 const char* str="666666\n"; 17 while(cnt--){ 18 write(fd,str,strlen(str)); 19 } 20 21 close(fd);//关闭文件 22 23 return 0; 24 }
运行结果:
> 文件名 ,前面什么都不写,直接重定向 + 文件名:
$ > log.txt
这算是一个小技巧吧
感谢阅读!!!!!!!!!!!