我们说文件IO主要分为下面几个重点
我们说一个文件的描述符,一个进程可以打开多个文件,一个进程启动时就会打开0,1,2,也就是输入输出和err,系统中会存在多个打开的文件,如何管理文件,先描述后组织,用一个数据结构就可以描述和组织struct_file
文件描述符的本质是数组的下标,他和FILE*的关系
int main()
{
FILE*fp=fopen("log.txt","w");
if(NULL==fp){
perror("open");
return 1;
}
fclose(fp);
}
w和a一样吗?
a是追加:也是写入,前者是从头开始写,后者是从结尾开始写(追加),我们恰恰可以想到重定向’>‘和追加重定向’>>’
r Open text file for reading.
The stream is positioned at the beginning of the file.
r+ Open for reading and writing.
The stream is positioned at the beginning of the file.
w Truncate(缩短) file to zero length or create text file for writing.
The stream is positioned at the beginning of the file.
w+ Open for reading and writing.
The file is created if it does not exist, otherwise it is truncated.
The stream is positioned at the beginning of the file.
a Open for appending (writing at end of file).
The file is created if it does not exist.
The stream is positioned at the end of the file.
a+ Open for reading and appending (writing at end of file).
The file is created if it does not exist. The initial file position
for reading is at the beginning of the file,
but output is always appended to the end of the file.
打开文件之后我们创建的文件路径在哪里?
注意,创建文件一定是当前路径,这和可执行程序的位置没有任何关系,和启动程序的位置有关
Linux中一切皆文件,键盘和显示器是文件吗?那为什么C语言中没有打开文件,我们也可以直接printf输出,scanf输入?
任何进程在运行的时候OS默认都会打开三个输入输出流,分别是stdin,stdout,stderror,仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
演示示例:利用stdout输出到显示屏
#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;
}
fopen究竟在干什么?
- 给调用的用户申请struct FILE结构体变量,并返回地址(FILE*)
- 在底层通过open打开文件,并返回fd,把fd填充到file变量的fileno
类似的fread,fwite,fputs,fgets等都是通过FILE* 指针找到fd
在Linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针,已打开的文件在内核中用file结构体表示,文件描述符表中的指针指向file结构体。
标准输入(stdin)的文件描述符是 0
标准输出(stdout)的文件描述符是 1
标准错误(stderr)的文件描述符是 2
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
文件描述符的分配规则:从当前未被分配的最小整数处分匹配。
运行下面的代码我们可以看到一个现象
#include
#include
#include
#include
int main()
{
int fd1 = open("log.txt", O_WRONLY|O_CREAT, 0666);
printf("fd1: %d\n", fd1);
int fd2 = open("log.txt", O_WRONLY|O_CREAT, 0666);
printf("fd2: %d\n", fd2);
int fd3 = open("log.txt", O_WRONLY|O_CREAT, 0666);
printf("fd3: %d\n", fd3);
int fd4 = open("log.txt", O_WRONLY|O_CREAT, 0666);
printf("fd4: %d\n", fd4);
int fd5 = open("log.txt", O_WRONLY|O_CREAT, 0666);
printf("fd5: %d\n", fd5);
}
我们发现文件描述符是从3开始标记的,那么0,1,2在哪里呢?
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器另外文件描述符的分配规则是从小到大分配
一个进程可以打开多少个文件描述符?
系统当中一个进程打开文件描述符的数量是由限制的,可以通过"ulimit -a"指令查看
这里的"open files"的大小就是一个进程可以打开文件描述符的数量
"open files"的大小可以通过ulimit -n [num]修改,eg:ulimit -n 10000,将一个进程可以打开的文件描述符数量限制为10000
我们知道,当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。
而task_struct当中有一个指针,该指针指向一个名为files_struct的结构体,在该结构体当中就有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符。
当进程打开log.txt文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可。
磁盘文件 V.S. 内存文件
我们说上一问的文件是内存文件,下面思考一下磁盘文件,我们在磁盘中存的时候就是存的是文件的内容+属性
磁盘文件由两部分构成,分别是文件内容和文件属性。文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名、文件大小以及文件创建时间等信息都是文件属性,文件属性又被称为元信息。
内存的文件的打开主要是文件的属性信息,把属性信息写到文件中,而主要的读写的内容会延后式的慢慢加载数据(缓冲区)
我们可以尝试用系统接口打开文件试试看
int main()
{
close(1);
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666); // 1
if (fd < 0)
{
return 1;
}
}
什么是系统函数参数传参标志位?
其实他是一个通过定义一组宏引用传参时传宏,内部进行宏判断可以通过bit位的方式来传递多种选项给函数
int是32bit,所以说理论上可以传递32种标志位,其中这些有关的宏中32个bit位只有一个是1
我们可以先来查看一个open函数,open中的flags就是一个标志位,它是一个int类型的
#include
#include
#include
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
**pathname: **要打开或创建的目标文件
若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:-1
实际上这里所谓的文件描述符本质上是一个指针数组的下标,指针数组当中的每一个指针都指向一个被打开文件的文件信息,通过对应文件的文件描述符就可以找到对应的文件信息。
当使用open函数打开文件成功时数组当中的指针个数增加,然后将该指针在数组当中的下标进行返回,而当文件打开失败时直接返回-1,因此,成功打开多个文件时所获得的文件描述符就是连续且递增的。
而Linux进程默认情况下会有3个缺省打开的文件描述符,分别就是标准输入0、标准输出1、标准错误2,这就是为什么成功打开文件时所得到的文件描述符是从3开始进程分配的。
要查看这些参数标志位我们可以查找这个路径下的fcntl-linux.h文件
/usr/include/bits/fcntl-linux.h
假如我们想要满足同时两个标志位的话,只要按位或起来就可以,比如下面这样
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
open函数的第三个参数是mode,表示创建文件的默认权限。
例如,将mode设置为0666,则文件创建出来的权限如下:
但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)
。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。
若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用umask
函数将文件默认掩码设置为0。
打开获取fd之后可以关闭
close(fd);
NAME
read - read from a file descriptor
SYNOPSIS
#include
//参数[你从哪里读,读到哪里去,要读几个字节]
ssize_t read(int fd, void *buf, size_t count);
理论上count是期望读的字节数,返回值是实际读的字节
NAME
write - write to a file descriptor
SYNOPSIS
#include
//参数[你写到哪里去,你从哪里读,要读几个字节]
ssize_t write(int fd, const void *buf, size_t count);
理论上也是一样的返回值
使用示例:
int countt = 5;
const char *msg = "hello there\n";
while(count){
write(fd1, msg, strlen(msg));
count--;
}
strlen这里要不要+1,留一个’\0’?
不需要,多一个字符的话会出现乱码,因为结尾是’\0’的规则是C语言的,C++中是string就没有’\0’,而这里的文件不认你C的规则
我们发现这个系统接口和C接口好像啊,所以他们之间是不是有什么关系呢?
其实在Linux系统中C语言的fopen调用的就是open接口,同理fclose调用的就是close接口,类似的windows也会提供它自己的接口供语言调用
而正是C语言这些的接口完成了这些个封装,相较于系统级接口,其实实质上1. 更加方便调用 2. 而且满足了跨平台性
一个进程可以打开多个文件吗?文件管理 | 内存文件
当然可以,那么多个进程也能打开多个文件,系统中,事实上,在任何时刻都可能存在大量的已经打开的文件,这就是是操作系统的文件管理,回到管理,我们对文件的管理也是先描述再组织,利用进程,再内存中描述数据结构,一个文件用一个struct,一堆文件就用链表把多个struct链接起来
文件有几部分,是不是只保存文件的内容?
文件除了它的内容以外还是有文件的属性(元信息),文件实质上=内容+属性,所以不是说我文件里面有256KB最后就显示该磁盘文件的大小是256KB,还有属性所代表大小
文件指针:C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括缓冲区和文件描述符。而文件描述符是文件描述符表的一个索引,也就是说c语言的文件指针是Linux系统中对文件描述符的一种封装。
在/usr/include/libio.h
中是这么定义_IO_FILE结构体的
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
//读缓冲区
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
//写缓冲区
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //文件描述符的数值
...
文件指针指向的结构体,可以发现文件指针结构体里包含有文件描述符,说明文件指针是对文件描述符的一种封装。
文件指针是C语言库里的提供的一个结构体,文件描述符是系统调用接口
为什么系统已经有了文件描述符,库里面还要对其做一层封装呢?
- 方便程序员使用
- 可以提高程序的移植性
FILE结构体里面还有缓冲区:
输出重定向
在明确了文件描述符的概念及其分配规则后,现在我们已经具备理解重定向原理的能力了。你会发现重定向的本质就是修改文件描述符下标对应的struct file*的内容。
#include
#include
#include
#include
#include
int main()
{
umask(0);
close(1);
int fd = open("myfile", O_WRONLY|O_CREAT, 0666);
if(fd < 0){
perror("open");
return 1;
}
printf("hello there\n,printf");
fprintf(stdout,"hello there,fprintf\n")
fputs("hello there\n,fputs", stdout);
fflush(stdout);//需要刷新输出缓冲区
close(fd);
return 0;
}
我们发现,本来应该输出到显示器上的内容,输出到了文件myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <
printf是C库当中的IO函数,一般往stdout中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。
输出重定向原理:
追加重定向
衍生来说,所谓的追加重定向就是打开文件的时候或上一个追加的宏就可以了
输入重定向
输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据。
输入重定向的原理:
如果我们想让本应该从“键盘文件”读取数据的scanf函数,改为从log.txt文件当中读取数据,那么我们可以在打开log.txt文件之前将文件描述符为0的文件关闭,也就是将“键盘文件”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是0。
#include
#include
#include
#include
#include
int main()
{
close(0);
int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
char str[40];
while (scanf("%s", str) != EOF){
printf("%s\n", str);
}
close(fd);
return 0;
}
这是因为:scanf函数是默认从stdin读取数据的,而stdin指向的FILE结构体中存储的文件描述符是0,因此scanf实际上就是向文件描述符为0的文件读取数据。
上述操作的重定向显然是不太合理的,要先关闭文件然后再打开文件这样重定向不能完成很多情况
dup2就是可以一行命令完成重定向,也能够保证我们可以指定式重定向,意思是将oldfd重定向至newfd
NAME
dup, dup2, dup3 - duplicate a file descriptor
SYNOPSIS
#include
int dup(int oldfd);
int dup2(int oldfd, int newfd);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include /* Obtain O_* constant definitions */
#include
int dup3(int oldfd, int newfd, int flags);
DESCRIPTION
These system calls create a copy of the file descriptor oldfd.
dup() uses the lowest-numbered unused descriptor for the new descriptor.
dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:
* If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.
* If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.
示例:
//int fd = open("./README.TXT",O_RDWR|O_APPEND,0777);
dup2(fd,STDIN_FILENO);
实际上通过调用dup2(fd,STDIN_FILENO);fd和STDIN_FILENO之间建立了一种关系。本来所有函数的输出都要往终端输出哪里走,但是通过调用dup2函数,终端输出关闭了(也就是若newfd原来已经打开了一个文件,则先关闭这个文件),那么终端输出关闭后这些输出往哪里去呢?当然是往我们新复制的文件描述符这里了。所有相当于是把newfd重定向至了oldfd。
把old的内容拷贝到new中,所以最终new和old中的内容都会是old的内容,注意dup完之后是很难恢复的
测试实例:
我们将打开文件log.txt时获取到的文件描述符和1传入dup2函数,那么dup2将会把fd_arrya[fd]的内容拷贝到fd_array[1]中,在代码中我们向stdout输出数据,而stdout是向文件描述符为1的文件输出数据,因此,本应该输出到显示器的数据就会重定向输出到log.txt文件当中。
#include
#include
#include
#include
#include
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
close(1);
dup2(fd, 1);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
追加重定向
只需在输出只写的基础上添加O_APPEND选项
int fd = open("log.txt", O_WRONLY | O_APPEND); // a
if (fd < 0)
{
perror("open");
return 1;
}
// fd, 1
close(1);
dup2(fd, 1);
输入重定向
//int fd = open("./README.TXT",O_RDWR|O_CREAT,0777);
dup2(fd,0);
我们可以增加重定向功能给我们的shell,关键是找到大于符号,如果在命令行中找到了>
的话,将他置为’\0’,字符串就拆成两块,处理完了之后就是前面的是指令后面的是指向的文件
首先我们需要知道
子进程会继承父进程打开的文件的信息
进程替换不会影响打开文件的信息
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define LEN 1024 //命令最大长度
#define NUM 32 //命令拆分后的最大个数
int main()
{
int type = 0; //0 >, 1 >>, 2 <
char cmd[LEN]; //存储命令
char* myargv[NUM]; //存储命令拆分后的结果
char hostname[32]; //主机名
char pwd[128]; //当前目录
while (1){
//获取命令提示信息
struct passwd* pass = getpwuid(getuid());
gethostname(hostname, sizeof(hostname)-1);
getcwd(pwd, sizeof(pwd)-1);
int len = strlen(pwd);
char* p = pwd + len - 1;
while (*p != '/'){
p--;
}
p++;
//打印命令提示信息
printf("[%s@%s %s]$ ", pass->pw_name, hostname, p);
//读取命令
fgets(cmd, LEN, stdin);
cmd[strlen(cmd) - 1] = '\0';
//实现重定向功能
char* start = cmd;
while (*start != '\0'){
if (*start == '>'){
type = 0; //遇到一个'>',输出重定向
*start = '\0';
start++;
if (*start == '>'){
type = 1; //遇到第二个'>',追加重定向
start++;
}
break;
}
if (*start == '<'){
type = 2; //遇到'<',输入重定向
*start = '\0';
start++;
break;
}
start++;
}
if (*start != '\0'){ //start位置不为'\0',说明命令包含重定向内容
while (isspace(*start)) //跳过重定向符号后面的空格
start++;
}
else{
start = NULL; //start设置为NULL,标识命令当中不含重定向内容
}
//拆分命令
myargv[0] = strtok(cmd, " ");
int i = 1;
while (myargv[i] = strtok(NULL, " ")){
i++;
}
pid_t id = fork(); //创建子进程执行命令
if (id == 0){
//child
if (start != NULL){
if (type == 0){ //输出重定向
int fd = open(start, O_WRONLY | O_CREAT | O_TRUNC, 0664); //以写的方式打开文件(清空原文件内容)
if (fd < 0){
error("open");
exit(2);
}
close(1);
dup2(fd, 1); //重定向
}
else if (type == 1){ //追加重定向
int fd = open(start, O_WRONLY | O_APPEND | O_CREAT, 0664); //以追加的方式打开文件
if (fd < 0){
perror("open");
exit(2);
}
close(1);
dup2(fd, 1); //重定向
}
else{ //输入重定向
int fd = open(start, O_RDONLY); //以读的方式打开文件
if (fd < 0){
perror("open");
exit(2);
}
close(0);
dup2(fd, 0); //重定向
}
}
execvp(myargv[0], myargv); //child进行程序替换
exit(1); //替换失败的退出码设置为1
}
//shell
int status = 0;
pid_t ret = waitpid(id, &status, 0); //shell等待child退出
if (ret > 0){
printf("exit code:%d\n", WEXITSTATUS(status)); //打印child的退出码
}
}
return 0;
}
什么叫做一个进程在默认创建的时候就打开了0,1,2呢?
OS把键盘,输出显示器和错误显示器形成struct files,然后默认让files_struct中的0,1,2分别指向标准输出,标准输入和标准错误
1和stdout/stdin/stderr的关系
其实stdout/stdin/stderr是一个结构体,是struct FILE类型的结构体,它内部肯定有一个成员变量是fd
在之前的重定向中,我们发现如果我们close(1)之后只是输出到stdout仍是没有在显示器中看到有输出,因为此时我们没有刷新缓冲区,所以在执行fflush(stdout)之后我们才看到有输出,可是我们之前明明就已经close(fd)了呀,为什么还要fflush呢?这就涉及到了缓冲区知识
缓存有三种:
⚓️ 为什么要有缓冲区?
我们都知道硬盘的速度要远低于 CPU,它们之间有好几个数量级的差距,当向硬盘写入数据时,程序需要等待,不能做任何事情,就好像卡顿了一样,用户体验非常差。计算机上绝大多数应用程序都需要和硬件打交道,例如读写硬盘、向显示器输出、从键盘输入等,如果每个程序都等待硬件,那么整台计算机也将变得卡顿。
但是有了缓冲区,就可以将数据先放入缓冲区中(内存的读写速度也远高于硬盘),然后程序可以继续往下执行,等所有的数据都准备好了,再将缓冲区中的所有数据一次性地写入硬盘,这样程序就减少了等待的次数,变得流畅起来。
缓冲区的另外一个好处是可以减少硬件设备的读写次数。其实我们的程序并不能直接读写硬件,它必须告诉操作系统,让操作系统内核去调用驱动程序,只有驱动程序才能真正的操作硬件。
从用户程序到硬件设备要经过好几层的转换,每一层的转换都有时间和空间的开销,而且开销不一定小;一旦用户程序需要密集的输入输出操作,这种开销将变得非常大,会成为制约程序性能的瓶颈。
这个时候,分配缓冲区就是必不可少的。每次调用读写函数,先将数据放入缓冲区,等数据都准备好了再进行真正的读写操作,这就大大减少了转换的次数。实践证明,合理的缓冲区设置能成倍提高程序性能。
缓冲区其实就是一块内存空间,它用在硬件设备和用户程序之间,用来缓存数据,目的是让快速的 CPU 不必等待慢速的输入输出设备,同时减少操作硬件的次数。
缓冲区在哪里?是谁提供的?
我们用一个测试用例来引出:
#include
#include
#include
int main()
{
//C语言函数
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
//system
const char *msg = "hello write\n";
write(1, msg, strlen(msg));
fork();
return 0;
}
我们发现如果我们只是执行该文件的话,效果如下:
但是一旦我们重定向到一个文件中时却变成了5行:
这恰恰说明了:C接口打了两次,OS的API打印了一次
所以说重定向和非重定向会改变进程的缓冲方式
我们发现printf 和fwrite (库函数)都输出了2次,而write 只输出了一次(系统调用)。为什么呢?fork有关!
一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printf fwrite 库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
但是进程退出之后,会统一刷新,写入文件当中。
但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
write 没有变化,说明没有所谓的缓冲。
综上: **printf fwrite 库函数会自带缓冲区,而write 系统调用没有带缓冲区。**另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区。那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是write 没有缓冲区,而printf fwrite有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。
在/usr/include/libio.h中有提到缓冲区
解释为什么已经close(fd)了呀,为什么还要fflush呢?
首先需要下面的图理解关系:关键是用户缓冲区和内核缓冲区的区分
我们说用户在用户缓冲区刷新,是不可能直接写到磁盘或者是输出到显示器上的,所以肯定是经过了内核缓冲区的一套刷新机制,然后再到磁盘和显示器,也就是fflush把进程缓冲区的数据刷新到内核缓冲区,fsync把内核缓冲区的数据刷新到物理媒介上
那为什么没有fflush(stdout)就不会输出我printf在fd和fputs在stdout的内容呢?因为开始的close(1)导致了重定向,所以从行缓冲变为了全缓冲(上一个问题中提到的:一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲),假设我现在不fllush(stdout)或者是fclose(stdout)的话,我们调用的是close(fd)时,fd:1已经关了,所以没有办法通过fd的指针找到文件刷新了,数据无法刷新到内核缓存区中,就失败了
inode是一个文件的属性集合,Linux中的几乎每一个文件都有一个inode
一般ls打印的是文件的属性,cat打印的是文件的内容
before inode 先看一下文件系统:
要知道文件系统得先知道什么是磁盘?
磁盘是在我们的计算机中的几乎唯一一个机械设备,磁盘具有永久性,但是是掉电易失的存储介质,目前所有的普通文件都是存储在磁盘中的。这里我们可以理解磁盘的本质其实是线性结构,然而又因为整个磁盘是很大的,我们可以把它划分成多个扇区,代销也可以不同,好处就是便于管理,因为只要管理好一个区就可以管理好所有区
BootSector是一个启动块,后面的是一个分区中的不同分组
**Block Group:**ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。
**超级块(Super Block):**存放文件系统本身的结构信息。记录的信息主要有:block 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载(分配磁盘符)的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
GDT,Group Descriptor Table:块组描述符,描述块组属性信息
块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
**inode位图(inode Bitmap):**每个bit表示一个inode是否空闲可用。
**i节点表(Inode table):**存放文件属性,如: 文件大小,所有者/组,最近修改时间,权限等,注意inode中是没有文件名的,只有编号
数据区(Data blocks):存放文件内容,数据区也分一个个块号,这些块号都应该写入inode,记录一个数组,通过数组,我就可以访问其中的不同块号
inode table里面有哪些inode已经被占用?哪些没有?Data blocks里面有哪些block已经被占用?哪些没有?
inode bitmap,通过极限提高查找效率,可以知道哪些inode是没有占用的,哪些是占用的,其中可以是为0的就是没有被占用,为1的就表示是被占用的
同样的也有block bitmap,思想是一样的
注意点:
创建一个新文件需要哪些操作?
- 存储属性
内核先找到一个空闲的i节点(这里是889012)。然后把这里的比特位置为1,说明这里要被占用了,然后把这个inode table的空间申请给这个文件,内核把文件信息记录到其中。- 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。- 记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。- 添加文件名到目录
☢️ 新的文件名abc。linux如何在当前的目录中记录这个文件?
内核将入口(889012,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
写入1KB数据的过程?删除一个文件的过程?
扫描bitmap找到文件对应的inode,再通过inode找到对应的data block,发现文件没有申请过空间的话,就还需要再block中申请空间,扫描block bitmap,置为1,把block id写入block,再写入数据1KB
删除文件只需要,在inode bitmap置为0,把inode对应的block,把block的bitmap置为0
因此拷贝文件慢,删除文件快,因此说删除文件是可以恢复的,但是要注意删除的文件也是会被覆盖的
如何理解目录?目录创建的过程
目录的inode的创建是一样的,目录属性是类似的,目录的内容放的是,当前目录下文件名和对应文件的inode指针(inode号),目录文件里面放的是文件名和inode的映射,这些内容算作数据文件
ls在做什么?ls -l做了什么?cat是在做什么?
ls就是找到当前目录的inode对应的数据内容位置,只需要文件名就可以了
ls -l则需要找到当前目录的inode,就找到了当前的数据块,就找到了当前目录下的文件名,再通过文件名找到每个文件的inode指针,再去跑到inode table找到文件的权限,所属组,所属成员,大小,创建时间和文件名,拼接字符串输出
cat一个文件,找出当前文件的inode属性,在inode中找到数据,然后把内容显示出
进入一个目录需要什么权限?读取一个目录需要什么权限?在目录下创建文件需要什么权限?
进入一个目录需要执行权限x,读取一个目录需要读取数据区,所以需要读权限r,在目录下创建文件写权限w(需要向当前目录写入文件名和inode的映射关系)
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,在shell中的做法
软连接和硬链接的区别是:
例如有一个文件 a.txt
,文件内容长度是 1024
个字节,存放在硬盘上的某个块(block
)中,假设就是第 10000
个块吧。
那么这个文件对应的 inode
节点中,就会把 10000
这个块记录下来。
同时,它还有一个 links
字段,表示:当前这个 inode
对应一个文件,此时 inode.links
的值为 1。(引用计数)
此时,如果我们用另一个文件名 a_hard_link.txt
,也来表示 a.txt
这个文件。
也就是说:虽然我们用了 2
个文件名称,但是本质上指向同一个文件,内容都指向第 10000
个块中存储的文件内容。
Linux
系统中提供了硬链接来支持这样的目的,它仅仅是把 inode
节点中的 links
字段的值加1即可,也就是 inode.links 的值变成了 2。
ln [filename] [hard-link name]
真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个inode。
pokemon和pikachu的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 1063107 的硬连接数为2。
我们在删除文件时干了两件事情:
关键在于硬链接有什么用?
硬链接的本质相当于取了一个别名
- 允许一个文件拥有多个有效路径名,这样用户就可以建立硬链接到重要的文件,以防止“误删”源数据。这时候就可以方便文件多人共享当很多人同时对同一个文件进行维护的时候,如果大家都直接操作这个文件,万一不小心把文件删除了,大家就都玩完了!此时,可以在每个人自己的私人目录中,创建一个硬链接。每次只需要对这个硬链接文件进行操作,所有的改动会自动同步到目标文件中。由于每个人都是操作硬链接文件,即使不小心删除了,也不会导致文件的丢失。因为删除硬链接文件,仅仅是把该文件的
inode
节点中的links
值减 1 而已,只要不为0
,就不会真正的删除文件。- 同时方便目录通过相对路径的方式进行跳转,其实我们在
cd .
和cd ..
的时候这个.
就是硬链接,你创建了子文件夹会发现当前文件夹的硬链接数就变了- 文件备份.在备份的时候,如果是实实在在的拷贝一份,那真的是太浪费磁盘空间,特别是对于我这种只有 256G 硬盘空间的笔记本。此时,就可以利用硬链接功能,既实现文件备份的目的,又节省了大量的硬盘空间.很多备份工具利用的就是硬链接的功能,包括
git
工具,当克隆本地的一个仓库时,执行clone
指令,git
并不会把仓库中的所有文件拷贝到本地,而仅仅是创建文件的硬链接,几乎是零拷贝!
补充:硬链接存在 2 个限制:
不用进入目录我能不能知道目录里面又几个子目录?
可以,是当前目录的硬链接-2
为了克服硬链接的 2
个限制,软链接被引入进来了。
软链接也叫符号链接,它是一个独立的文件。
软链接文件的内容是一个文本字符串,存储的是目标文件(即:链接到的文件)的路径名。
这个路径名可以指向任意一个文件系统的任意文件或者目录,甚至可以指向一个不存在的文件。
与创建硬链接不同的是:当我们创建了一个软链接之后,操作系统会创建一个新的 inode 来表示这个软链接文件。
如果我们把源文件删除掉之后,inode
节点会被删除掉,当真正的目标文件被删除之后,快捷方式也就没有存在的意义了。
ln -s [filename] [soft-link name]
软连接的本质有点像是快捷方式,这个文件也有自己的数据块,保存的是指向文件所在的路径和文件名
软连接有什么用?
- 灵活切换不同版本的目标程序 :在开发的过程中,对于同一个工具软件,可能要安装多个不同的版本,例如:
Python2
和Python3
,g++ 4.8
和g++ 7.2
等等。此时就可以通过软链接来指定当前使用哪个版本。- 动态库版本管理 (后面有提到)
- 快捷方式
参考https://cloud.tencent.com/developer/article/1837822
###acm
acm指的是文件的三个时间:
想要查看文件修改的时间的话可以用命令
stat [filename]
modify一般个change是联动的,modify改了change也会改,不过反之可能不会连带改,比如说我只修改文件的属性,比如权限,不修改内容,只有change变了
动静态库是一个可执行程序的半成品(本质是一堆.o的集合,不包含main,但是包含了大量的方法),静态库与动态库都是二进制的程序代码的集合。将程序编写成库提供给第三方使用,这样做的好处是不会造成源码泄漏,而且调用者不用关心内部实现。
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
☯️ windows中的动静态库?Linux动静态库的命名?
在windows中.dll是动态库,而.lib是静态库
库的名字,是去除lib和后缀之后名字:libc.so.6->c库
查看可执行文件所依赖的库文件
ldd [可执行文件]
我们发现这里的动态库采用的是软连接方式,同时我们发现了软链接文件的属性是l
库 | 缺点 | 优点 | 特点 |
---|---|---|---|
静态库 | 自身比较大,占空间 静态链接浪费空间,多个C静态程序加载的时候一定会有在内存中的大量重复代码 |
与库无关了,不需要库 | N.A. |
动态库 | 必须依赖库,没有库没办法运行 | 通过地址空间共享的 | 基本都是代码,是不能被写入的 |
补充描述:在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
测试用例:add.c/add.h/sub.c/sub.h
[root@localhost linux]# ls
add.c add.h main.c sub.c sub.h
# 直接gcc
[root@localhost linux]# gcc -c add.c -o add.o
[root@localhost linux]# gcc -c sub.c -o sub.o
[root@localhost linux]# ar -rc libmymath.a add.o sub.o
ar是gnu归档工具,rc表示(replace and create)
[root@localhost linux]# ar -tv libmymath.a
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 add.o
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 sub.o
Makefile操作生成静态库
补充Makefile:
mylib=libcal.a
CC=gcc
$(mylib):add.o sub.o
ar -rc $(mylib) $^
%.o:%.c # 这里的%表示通配符,表示任意一个.o文件的依赖是.c
$(CC) -c $< # 这里的$<是把.c文件一个个写上
.PHONY:clean
clean:
rm -f $(mylib) *.o
.PHONY:output
output:
mkdir -p mathlib/lib
mkdir -p mathlib/include
cp *.h mathlib/include
cp *.a mathlib/lib
在这个Makefile的基础上我们进行make,会发现达到了结果
这个时候我们需要使用静态库,我们怎么使用呢?
首先比如我们写了一个test.c,我们需要包上头文件,这里推荐使用尖括号
///
///test.c//
///
#include
#include
int main()
{
int a=10;
int b=20;
int c=my_add(a,b);
return 0;
}
但是此时我们发现gcc还是找不到我们的add.h,那肯定,因为gcc不知道路径,不像系统库
gcc -o test test.c
gcc test.c -I./mathlib/include
现在还是报错,但是我们可以看到gcc已经找到了我们的my_add函数,这是因为现在我们的main函数中,还是不知道函数my_add是来自于add.h的
gcc test.c -I./mathlib/include -L./mathlib/lib
现在还是报错,因为有时候这个目录下会有很多库,但是具体要链接哪一个是不确定的,所以还得告诉它要链接哪一个库
所以我们通过-lcal
告诉我们的gcc是这个libcal.a的库,这样就过了
gcc test.c -I./mathlib/include -L./mathlib/lib -lcal
shared: 表示生成共享库格式
fPIC:产生位置无关码(position independent code)
库名规则:libxxx.so
gcc -fPIC -c sub.c add.c
gcc -shared -o libmymath.so *.o
使用的时候直接使用需要还是需要先告诉路径,才能够编译
gcc test.c -I mlib/include/ # -I不够
gcc test.c -I mlib/include/ -L mlib/lib # -I和-L还不够
gcc test.c -I mlib/include/ -L mlib/lib lcal # -I -L -l
此时还没有结束,之前是编译阶段,不过这个程序当我们运行的时候,他会形成一个进程,但是由于运行的时候不知道动态库在哪里,所以说还是找不到动态库的数据位置以加载到进程中
方法有三:
1、拷贝.so文件到系统共享库路径下, 一般指/usr/lib
2、更改LD_LIBRARY_PATH
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:[lib的绝对路径]
3、ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新
cat /etc/ld.so.conf.d/bit.conf
/root/tools/linux # 输出
ldconfig
如何制作动静态库?
都必须现状话为.o文件,然后打包
# 静态库: gcc -c ar -rc # 动态库 gcc -fPIC -c gcc -shared [name]
使用库?
给别人的,include(头文件集合)+lib(库文件)
静态库 -I -L -l
动态库 -I -L -l + export LD_LIBRARY_PATH
给别人库的时候,本质是给别人一份头文件(库的使用说明书)+一份库文件(.a/.so,库的实现),目标文件生成后,静态库删掉,程序照样可以运行。
-I #头文件在哪里
-L #指定库路径
-l #指定库名
其实我们看上一顿操作太麻烦了,其实事实上我们可以把它放到/usr/include/
系统库,这时安装好后还需要用-l选项不过可以省去-I和-L
也就是说安装库的过程就是拷贝到系统路径下,因此以后打好库的时候要给他人安装,只需要写一个脚本去安装库就可以了直接简单操作了
使用第三方库的时候一般要指明库名称
为什么说Linux下一切皆文件?
我们说磁盘,显示器,网卡,键盘,怎么理解这些东西是文件呢?这些东西被抽象成了文件。你可以使用访问文件的方法访问它们获得信息
这样做最明显的好处是,开发者仅需要使用一套 API 和开发工具即可调取 Linux 系统中绝大部分的资源。举个简单的例子,Linux 中几乎所有读(读文件,读系统状态,读 socket,读PIPE)的操作都可以用read函数来进行;几乎所有更改(更改文件,更改系统参数,写 socket,写 PIPE)的操作都可以用write函数来进行。
虚拟文件系统(Virtual File System, 简称 VFS),是 Linux 内核中的一个软件层,用于给用户空间的程序提供文件系统接口;同时,它也提供了内核中的一个抽象功能,允许不同的文件系统共存。系统中所 有的文件系统不但依赖 VFS 共存,而且也依靠 VFS 协同工作。
为了能够支持各种实际文件系统,VFS 定义了所有文件系统都支持的基本的、概念上的接口和数据 结构;同时实际文件系统也提供 VFS 所期望的抽象接口和数据结构,将自身的诸如文件、目录等概念在形式 上与VFS的定义保持一致。换句话说,一个实际的文件系统想要被 Linux 支持,就必须提供一个符合VFS标准的接口,才能与 VFS 协同工作。实际文件系统在统一的接口和数据结构下隐藏了具体的实现细节,所以在VFS 层和内核的其他部分看来,所有文件系统都是相同的。
user space的应用程序与VFS的接口就是系统调用,VFS与驱动程序的接口就是file_operations。通过file_operations方法将 设备类型的差异化屏蔽了,这就是Linux能够将所有设备都理解为文件的缘由
既然这样,那设备的差异化又该如何体现呢?在文件系统层定义了文件系统访问设备的方法,该方法就是 address_space_operations,文件系统通过该方法可以访问具体的设备。
在C语言中,可以通过函数指针,做到调用同一个方法,指向不同对象时可以执行不同的方法,从而实现多态的性质。我们在每个struct file当中包含一堆函数指针,这样,在struct file上层看来所有的文件都是调用统一的接口,不管底层具体是什么文件;在底层我们通过函数指针指向不同硬件的方法来实现我们的设备差异性的多态