- 重修C语言文件知识
- Linux文件知识
- 标记位传参
- 文件的系统调用
- 理解什么是文件
- 文件fd的分配规则
- 重定向
- C语言文件层面的缓冲区知识
重修C语言文件知识
- 打开文件操作
fopen
函数:
我们看一段代码,以写(w)
的形式来打开文件:
#include <stdio.h>#define FILE_NAME "log.txt"
int main()
{FILE* fp = fopen(FILE_NAME,"w");if(NULL==fp){perror("fopen");return 1;}fclose(fp);return 0;
}
一开始我们并没有创建文件,程序运行会自动创建一个log.txt
的文件
- 打印输出到文件
fprintf
函数
#include <stdio.h>#define FILE_NAME "log.txt"
int main()
{FILE* fp = fopen(FILE_NAME,"w");if(NULL==fp){perror("fopen");return 1;}fprintf(fp,"%s\n","hello world!");fclose(fp);return 0;
}
运行程序,fprintf
会发送格式化输出到流 stream
中
以w
的方式对文件进行操作,文件的内容会被先清空,再进行操作
- 打开文件操作以
读(r)
的方式,fgets
函数从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内
#include <stdio.h>
#include <string.h>#define FILE_NAME "log.txt"
int main()
{FILE* fp = fopen(FILE_NAME,"r");if(NULL==fp){perror("fopen");return 1;}char buffer[64];while(fgets(buffer,sizeof(buffer)-1,fp) != NULL){buffer[strlen(buffer)-1]=0;puts(buffer);}fclose(fp);return 0;
}
这个while循环可写可不写,这样写的作用也就是保证buffer最后一个字符是终止符号
下面输出的结果就是将log.txt
中的数据输出出来
4.打开文件操作以追加(a)
的方式
#include <stdio.h>
#include <string.h>#define FILE_NAME "log.txt"
int main()
{FILE* fp = fopen(FILE_NAME,"a");if(NULL==fp){perror("fopen");return 1;}int count=5;while(count){fprintf(fp,"%s:%d\n","hello world!!",count--);}fclose(fp);return 0;
}
打开文件相关方式:
- “r” “只读”,只允许读取,不允许写入。文件必须存在,否则打开失败
- “w” “写入”。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容
- “a” “追加”。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)
- “r+” “读写”。既可以读取也可以写入,也就是随意更新文件。文件必须存在,否则打开失败
- “w+” “写入/更新”,相当于w和r+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容
- “a+” “追加/更新”,相当于a和r+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)
- “t” 文本文件。如果不写,默认为"t"。
- “b” 二进制文件。
5.在linux下新建文件默认权限=0666,受到umask的影响,实际创建的出来的文件权限是: mask & ~umask
C语言有文件的操作接口,那么C++、Java、python、php、GO等语言同样也有文件操作接口,但是它们的接口都不一样。
而文件在哪?在磁盘,磁盘是硬件,而需要访问硬件都必须要操作系统OS来管理,使用OS给的文件级别的系统调用,操作系统只有一个,但是语言有很多个:库函数底层必须使用系统调用接口、库函数可以变化但是底层不变。
Linux文件知识
我们使用man 2来了解有关文件的系统调用知识,它与C语言有什么不同呢?
标记位传参
通过以下代码来解释什么是标记位传参:
#define ONE (1<<0)
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)void show(int flag)
{if(flag & ONE)printf("one\n");if(flag & TWO)printf("two\n");if(flag & THREE)printf("three\n");if(flag & FOUR)printf("four\n");
}
int main()
{show(ONE);printf("---------\n");show(TWO);printf("---------\n");show(ONE | TWO);printf("---------\n");show(ONE | TWO | THREE);printf("---------\n");show(ONE | TWO | THREE | FOUR);return 0;
}
标记对应比特位,每一个宏对应的数值,只有一个比特位是1,彼此不会重叠,如果想要互相结合就或(|)
,函数里面通过与(&)
来判断,这样就相当于可以传入多个参数
文件的系统调用
1.系统调用打开文件open
,
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数: 前三个常量,必须指定一个且只能指定一个
-
O_RDONLY: 只读打开
-
O_WRONLY: 只写打开
-
O_RDWR : 读写打开
-
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
-
O_APPEND: 追加写
使用系统调用来打开文件O_WRONLY
:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FILE_NAME "log.txt"
int main()
{int fp = open(FILE_NAME,O_WRONLY);if(fp < 0){perror("open");return 1;}close(fp);return 0;
}
在C语言中我们使用读的形式打开文件可以直接成功,但是在系统调用中我们使用只使用读的形式访问文件,是会失败的。
我们必须要更改代码为:
int fp = open(FILE_NAME,O_WRONLY | O_CREAT);
更改后创建出来的文件是乱码:
凭什么认为Linux一创建文件就按照比如666、777去创建?我们在C语言中使用的是已经封装过的系统调用,他会自动生成权限,而系统调用没有这些东西,他需要自己去传参,所有我们最后还需要传入作为权限的参数:
int fp = open(FILE_NAME,O_WRONLY | O_CREAT, 0666);
更改umask的值
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FILE_NAME "log.txt"
int main()
{umask(0);int fp = open(FILE_NAME,O_WRONLY | O_CREAT,0666);if(fp < 0){perror("open");return 1;}close(fp);return 0;
}
我们在创建访问之前将umask的值更改为0,使用我们传入的权限值去初始化,最后这个log.txt
的权限值就是666
我们再访问shell中umask的值发现它还是0002
,这是为什么,我们不是刚刚已经修改了,而且创建出来的文件权限也是按照更改后的umask初始的?这是因为我们程序里面的umask是这个子进程在执行,而与shell这个父进程无关系,子进程只能改变自己的文件权限,所以我们更改的时候不会影响shell。
- 向文件写入
write
ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int.
open函数返回值:
我们以读的形式打开(或创建)文件,并打印出open
函数的返回值
int main()
{umask(0);int fp = open(FILE_NAME,O_WRONLY | O_CREAT,0666);if(fp < 0){perror("open");return 1;}printf("fp:%d\n",fp);close(fp);return 0;
}
为什么这个打印的值是3呢?且看目录中的理解什么是文件详细讲述
ssize_t write(int fd, const void *buf, size_t count);
读写文件有两种读写方案:文本类、二进制类(而这些文件读取的分类是语言本身提供的)
而操作系统就很简单粗暴,直接以void*方式返回,在操作系统看来都是二进制,操作系统只会管你要写几个字节,而不会管你具体内容(不管你是图片还是字符串什么的,只认二进制)
假如我们也想像C语言那样写入hello word!! 并加上数字。一个是字符串、一个是数字,那么我们如何使用系统调用来实现呢?
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILE_NAME "log.txt"int main()
{umask(0);int fp = open(FILE_NAME,O_WRONLY | O_CREAT,0666);if(fp < 0){perror("open");return 1;}int count = 3;char arr[64];while(count){sprintf(arr,"%s:%d\n","hello world!!",count--);write(fp,arr,strlen(arr));}close(fp);return 0;
}
我们使用sprintf
将字符串写入数组中,然后使用write
将数组写入文件
我们的write(fp,arr,strlen(arr));
这个代码strlen
不需要加1来存储\0
,如果加了会出现乱码,因为以\0作为字符串的结束符是C语言规定的,和系统调用层面的文件操作没有关系
在C语言中,以写的方式打开文件会直接删除掉原数据,但是在系统调用的时候则是覆盖式的比如:
更改之前代码为:
sprintf(arr,"%s:%d\n","aaaaaa!!",count--);
我们在系统调用还需要传入一个O_TRUNC
,才能实现出在C语言中w
的效果:
int fp = open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);
而追加就是将O_TRUNC
跟换为O_APPEND
int fp = open(FILE_NAME,O_WRONLY | O_CREAT | O_APPEND,0666);
- 读文件
read
函数
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILE_NAME "log.txt"int main()
{umask(0);int fp = open(FILE_NAME,O_RDONLY);if(fp < 0){perror("open");return 1;}char buffer[1024];ssize_t num = read(fp,buffer,sizeof(buffer)-1);if(num>0)buffer[num]=0;printf("%s",buffer);close(fp);return 0;
}
sizeof(buffer)-1
这段作用是为填写终止符留出空间
buffer[num]=0;
这个语句的作用是添加结束符(0、\0、NULL),因为在C语言中的函数会自己写,而系统调用需要我们自己去写
理解什么是文件
文件操作的本质:进程 + 被打开文件 的关系
进程可以打开多个文件么?答案是肯定的,进程很多,同样系统一定会存在大量的被打开文件,那么这些被打开文件肯定也需要被操作系统OS管理起来,先描述再组织 -> OS为了管理对应的打开文件,必定会为文件创建对应的内核数据结构标识文件:struct file{}
(包含了大部分属性)
我们通过下面代码来理解文件:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FILE_NAME(number) "log.txt"#number//把参数转化为字符串,然后合起来
int main()
{int fd0 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666);int fd1 = open(FILE_NAME(2), O_WRONLY | O_CREAT | O_APPEND, 0666);int fd2 = open(FILE_NAME(3), O_WRONLY | O_CREAT | O_APPEND, 0666);int fd3 = open(FILE_NAME(4), O_WRONLY | O_CREAT | O_APPEND, 0666);int fd4 = open(FILE_NAME(5), O_WRONLY | O_CREAT | O_APPEND, 0666);printf("fd: %d\n", fd0);printf("fd: %d\n", fd1);printf("fd: %d\n", fd2);printf("fd: %d\n", fd3);printf("fd: %d\n", fd4);close(fd0);close(fd1);close(fd2);close(fd3);close(fd4);return 0;
}
通过下面的输出结果我们发现:为什么open
返回值从3
开始?012
又去了哪里?连续的小整数,一般情况我们只在数组下标
才有所对应
三个标准输入输出流:
- stdin—键盘
- stdout—显示器
- stderr—显示器
FILE* fp = fopen();
这个FILE是结构体,它里面有一个字段是文件描述符
我们增加下面代码:
printf("stdin->fd: %d\n", stdin->_fileno);printf("stdout->fd: %d\n", stdout->_fileno);printf("stderr->fd: %d\n", stderr->_fileno);
从输出结果我们可以看出,012在哪是什么了:三个标准输入输出提前占用了012
为什么这些输出的数字是为数组下标?且看下图
通过上面的学习,我们知道了文件描述符就是一个小整数:
- Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
- 0,1,2对应的物理设备一般是:键盘,显示器,显示器
上图可知:文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
文件描述符的本质,就是数组的下标!
文件fd的分配规则
我们首先做一个结果分析:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#define FILE_NAME "log.txt"
int main()
{//close(0);umask(0);int fd = open(FILE_NAME,O_WRONLY | O_CREAT,0666);if(fd < 0){perror("open");return 1;}printf("fd:%d\n",fd);close(fd);return 0;
}
打印的结果是3,这是我们上面解释过的
我们在main
函数刚开始的地方先close(0);
看看输出结果:
我们在main
函数刚开始的地方先close(2);
看看输出结果:
我们在main
函数刚开始的地方先close(0);close(2);
看看输出结果:
正常情况我们会自动打开三个标准输入输出,我们加载一个文件就会从3
开始:
但是,如果我们关闭掉一个会怎么样呢?
如果我们关闭close(1);
会出现什么情况呢?
我们发现,显示器上并没有打印的结果,这是为什么呢?且看下面解释:
printf("fd:%d\n",fd);fprintf(stdout,"fd:%d\n",fd);
这两个printf的结果都是一样:
上面结果可以说明printf本质上就是打印输出到stdout
中,现在我们将stdout
先关闭了,然后创建的新struct_file
占据了原本的stdout
的位置,所以我们可以认为现在的printf
输出应该输出到新创建的文件log.txt
中,但是我们cat log.txt
发现里面并没有数据,难道我们的结论是错误的?不是的,这里是因为缓冲区的缘故。
我们在最后刷新一下fflush(stdout);
,我们这里刷新的是stdout
,且最后输出的结果就是fd:1
,证明确实是分配的1
号位
本来输出打印应该打印到
stdout
显示器上,但是我们现在关闭close(1);
却将打印的结果打印到了新创建的文件中,这种特性叫做重定向
重定向
如果我们也想实现跟刚才一样的显示效果,将原本打印到显示器上的数据,输出到新建文件中去:
我们需要将1
号位中的数据替换为新建文件fd
,那么最终留下来的肯定是fd
,dup2
调用是将newfd
中的内容替换为oldfd
,留下来的是oldfd
,所以我们使用dup2调用需要:dup2(fd,1);
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILE_NAME "log.txt"
int main()
{umask(0);int fd = open(FILE_NAME,O_WRONLY | O_CREAT,0666);if(fd < 0){perror("open");return 1;}dup2(fd,1);printf("fd:%d\n",fd);fprintf(stdout,"fd:%d\n",fd);fflush(stdout);close(fd);return 0;
}
这样我们就完成了一个重定向功能
追加重定向O_APPEND + dup2(fd,1);
:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FILE_NAME "log.txt"
int main()
{umask(0);int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_APPEND,0666);if(fd < 0){perror("open");return 1;} dup2(fd,1);printf("fd:%d\n",fd);fprintf(stdout,"fd:%d\n",fd);const char* msg = "hello world";write(1, msg, strlen(msg));fflush(stdout);close(fd);return 0;
}
持续运行./Test
,将输出内容追加到log.txt
中
输入重定向dup2(fd,0);
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FILE_NAME "log.txt"
int main()
{umask(0);int fd = open(FILE_NAME,O_RDONLY);if(fd < 0){perror("open");return 1;}dup2(fd,0);char arr[64];while(1){printf("输入> ");if(fgets(arr, sizeof(arr), stdin) == NULL)break;printf("%s",arr);}close(fd);return 0;
}
通过dup2(fd,0);
将log.txt
里面的数据用做标准输入,while循环的作用是将标准输入中的数据拿出来存放入数组中,然后将其打印出来
如果我们在父进程中创建子进程,然后这个子进程做重定向操作会影响父进程吗?
程序替换,同样不会影响曾经进程打开过的重定向文件,重定向的各种操作属于内核数据结果,而程序替换则是磁盘与内存的代码数据替换,二者不会影响到。
常见的重定向有:> >> <
我们分别使用一下:
<
输出重定向
>
输入重定向
>>
追加重定向
C语言文件层面的缓冲区知识
我们先运行下面代码:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{// C语言接口printf("hello printf\n");fprintf(stdout, "hello fprintf\n");fputs("hello fputs\n", stdout);// 系统调用const char *msg = "hello write\n";write(1, msg, strlen(msg));return 0;
}
我们将输出的结果重定向到log.txt
文件中,可以发现打印的结果是一样的
但是如果我们将代码更改一下,在结尾创建子进程:
int main()
{// C语言接口printf("hello printf\n");fprintf(stdout, "hello fprintf\n");fputs("hello fputs\n", stdout);// 系统调用const char *msg = "hello write\n";write(1, msg, strlen(msg));fork();//do nothing -> return quitreturn 0;
}
还是一样的操作,但是在我们创建子进程后的,将输出重定向到log.txt
,我们发现C语言接口打印了两遍,而系统调用只打印了一次
缓冲区刷线策略问题:
缓冲区一定会结合具体的设备,定制自己的刷新策略
- 立即刷新 – 无缓冲
- 行刷新 – 行缓冲 – 显示器
- 缓冲区满 – 全缓冲 – 磁盘文件
- 用户强制刷新,比如fflush
- 进程退出 – 一般都要进行缓冲区刷新
上面的出现的现象,一定与缓冲区有关,且缓冲区一定不在内核中,因为如果在内核中,write
也会打印两次。我们之前谈论过的缓存区,都是指的用户级语言层面给我们提供的缓冲区,我们之前的输出输出操作都要传入 -> stdout stdin stderr
它们都是FILE*
类型的,而FILE
是一个结构体,这个结构体里面封装了fd
还有一个缓冲区
,所以我们刷新缓冲区都是fflush(文件指针)
、fclose(文件指针)
解释上面现象:
代码结束之前,进行创建子进程
1.如果我们没有进行重定向>
,看到了4条消息,stdout
默认使用的是行刷新,在进程fork
之前,三条C函数已经将数据进行打印输出到显示器上(外设),我们的FILE
内部,进程内部不存在对应的数据了
2.如果我们进行了重定向>
,写入文件不再是显示器,而是普通文件,采用的刷新策略是全缓冲,之前的3条c显示函数,虽然带了\n
,但是不足以让stdout
缓冲区写满,数据并没有被刷新!!!
执行fork
的时候,stdout
属于父进程,创建子进程时,紧接着就是进程退出,谁先退出,一定要进行缓冲区刷新(就是修改),修改就会发生写时拷贝,数据最终会显示两份
3. write
为什么没有呢?上面的过程都和wirte
无关,wirte
没有FILE
,而用的是fd
,就没有C提供的缓冲区
如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀