前言:本篇主要讲解底层文件系统接口,详细介绍 open 接口和它的 flags 参数 (即系统传递标记位),重点讲解 O_RDWR, O_RDONLY, O_WRONLY, O_CREAT 和 O_APPEND 这些操作模式。
hello.c写文件
#include
#include
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
#include
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
#include
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;
}
我们曾经讲过:文件 = 文件内容 + 文件属性 ①
文件属性也是数据!这意味着,即便你创建一个空文件,也要占据磁盘空间!所以:
② 文件操作 = 文件内容的操作 + 文件属性的操作
因此,在操作文件的过程中,既改变内容又改变属性的情况很正常,不要把它们割裂开来!
那么,所谓的 "打开" 文件,究竟在做什么? ③
"打开文件不是目的,访问文件才是目的!"
访问文件时,都是要通过 fread,fwrite,fgets... 这样的代码来完成对文件的操作的,
如果通过这些方式,那么 "打开" 文件就需要 将文件的属性或内容加载到内存 (memory) 中!
因为这是由冯诺依曼体系结构决定的,将来 \textrm{CPU} 要执行 fread,fwrite 来对文件进行读写的。
既然如此…… 是不是所有的文件都会处于被打开的状态呢?并不是! ④
那没有被打开的文件在哪里?
对于文件的理解,在宏观上我们可以区分成 内存文件 (打开的文件) 和 磁盘文件。 ⑤ (存储在磁盘中)
⑥ 通常我们打开文件、访问文件和关闭文件,是谁在进行相关操作?
运行起来的时候,才会执行对应的代码,然后才是真正的对文件进行相关的操作。
实际上是 进程在对文件进行操作! 在系统角度理解是我们曾经写的代码变成了进程。
进程执行调度对应的代码到了 fopen,write 这样的接口,然后才完成了对文件的操作。
当我执行 fopen 时,对应地就把文件打开了,所以文件操作和进程之间是撇不开关系。
结论:学习文件操作,实际上就是学习 "进程" 与 "打开文件" 的关系。 ⑦
文件的本质实际上是进程与打开文件之间的关系。
因此文件操作和进程有关系,我们写一下我们的代码,获取进程 ,查一下进程信息:
#include
#include
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
所以,当前路径更准确的说法应该是:在当前进程所处的工作路径。
" 当前路径指的是在当前进程所处的工作路径 "
只不过默认情况下 (默认路径) ,一个进程的工作路径在它当前所处的路径而已,这是可以改的。
所以我们在写文件操作代码时,不带路径默认是源代码所在的路径,注意是默认!而已!
由文件操作符 (mode) 参数来指定,常用的模式包括:
man fopen
r:只读模式,打开一个已存在的文本文件,允许读取文件。
r+:读写模式,打开一个已存在的文本文件,允许读写文件。
w:只写模式,打开一个文本文件并清除其内容,如果文件不存在,则创建一个新文件。
w+:读写模式,打开一个文本文件并清除其内容,如果文件不存在,则创建一个新文件。
a:追加模式,打开一个文本文件并将数据追加到文件末尾,如果文件不存在,则创建一个新文件。
a+:读写模式,打开一个文本文件并将数据追加到文件末尾,如果文件不存在,则创建一个新文件。
这里我们重点讲一下 a 和 a+
a 对应的是 appending 的首字母,意为 "追加" 。属于写入操作,不会覆盖源文件内容。
代码演示:测试追加效果
每次运行都会在 test.txt 里追加,我们多试几次看看:
a(append) 追加写入,可以不断地将文件中新增内容。(这让我联想到了追加重定向)
不同于 w,当我们以 w 方式打开文件准备写入的时候,其实文件已经先被清空了。
我们复习一下文本行输入函数 fgets,它可以从特定的文件流中,以行为单位读取特定的数据:
char* fgets(char* s, int size, FILE* stream);_
代码演示:fgets()
#include
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
/* 读什么就打什么 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 代码。
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问, 先来直接以代码的形式,实现和上面一模一样的代码:
写文件:
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
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));//类比write
if(s > 0){
printf("%s", buf);
}else{
break;
}
}
close(fd);
return 0;
}
当我们向文件写入时,最终是不是向磁盘写入?是!磁盘是硬件吗?就是硬件。
当我们像文件写入时,最后是向磁盘写入。磁盘是硬件,那么谁有资格向磁盘写入呢?
只能是操作系统!
既然是操作系统在写入,那我们自然不能绕开操作系统对磁盘硬件进行访问。
因为操作系统是对软硬件资源进行管理的大手子,你的任何操作都不能越过操作系统!
所有的上层访问文件的操作,都必须贯穿操作系统。
想要被上层使用,必须使用操作系统的相关的 系统调用 (syscall) !
回顾:: 如何理解 printf?我们怎么从来没有见过这些系统调用接口呢?
显示器是硬件,我们 printf 的消息打印到了硬件上,是你自己调用的 printf 打印到硬件上的,
但并不是你的程序显示到了显示器上,因为显示器是硬件,它的管理者只能是操作系统,
你不能绕过操作系统,而必须使用对应的接口来访问显示器,我们看到的 printf 一打,
内容就出现在屏幕上,实际上在函数的内部,一定是 调用了系统调用接口 的。
结论:printf 函数内部一定封装了系统调用接口。
所有的语言提供的接口,之所以你没有见到系统调用,因为所有的语言都被系统接口做了 封装。
所以你看不到对应的底层的系统接口的差别。为什么要封装?原生系统接口,使用成本比较高。
系统接口是 OS 提供的,就会带来一个问题:如果使用原生接口,你的代码只能在一个平台上跑。
直接使用原生系统接口,必然导致语言不具备 跨平台性 (Cross-platform) !
如果语言直接使用操作系统接口,那么它就不具备跨平台性,可是为什么采用封装就能解决?
封装是如何解决跨平台问题的呢?很简单:
" 穷举所有的底层接口 + 条件编译 "
结论:我们学习的接口,C 库提供的文件访问接口,系统调用。它们两具有上下层关系,C 底层一定会调用这些系统调用接口。
打开文件,在 C 语言上是 fopen,在系统层面上是 open。
open 接口是我们要学习的系统接口中最重要的一个,没有之一!所以我们放到前面来讲。
#include
#include
#include
int open(const char* pathname, int flags);
int open(const char* pathname, int flags, mode_t mode);
我们看到,这个 open 接口一个是两参数的,一个是三参数的,这个我们放到后面解释。
#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*
我们可以输入 man 2 open 看看如何设置 flags 参数,实际上就是设置文件操作模式的。
我们重点关注下面这几个文件操作模式,它们被定义在
O 实际上就是 Open 的意思,它们的用途通过名字不难猜:
返回值:
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
#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;
}
讲完了 flags 标记位,现在我们可以演示 open 接口的用法了。
int open(const char* pathname, int flags);
代码演示:是用 open() 打开 log.txt 文件没有就创建。
#include
#include
#include
#include
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,可以让我们以确定的权限打开文件,比如服务器要打开一个日志文件,权限就必须要按照它对应的权限设置好,不要采用系统的默认权限,可能会出问题。
在 C 语言中,关闭文件可以调用 fclose,在系统接口中我们可以调用 close 来关闭:
#include
int close(int fd);
该接口相对 open 相对来说比较简单,只有一个 fd 参数,我们直接看代码:
1 #include
2 #include
3 #include
4 #include
5 #include // 需引入头文件
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 }
没啥问题,就。。。。。没了。
文件打开和文件关闭都有了,我们总要干点事吧?现在我们来做文件写入!
在 C 语言中我们用的是 fprintf, fputs, fwrite 等接口,而在系统中,我们可以调用 write 接口:
#include
ssize_t write(int fd, const void* buf, size_t count);
write 接口有三个参数:
代码演示:向文件写入 6行信息
1 #include
2 #include
3 #include
4 #include
5 #include
6 #include // 需引入头文件
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
这算是一个小技巧吧
感谢阅读!!!!!!!!!!!