目录
一.C语言文件IO操作
1.写文件
2.读文件
3.默认打开的三个流
4.什么是 “当前路径”
二.系统文件IO操作接口
1.open函数
2.write , read ,close
3.关于文件描述符fd
三.FILE结构
1.FILE中的文件描述符
2.FILE的缓冲区理解
3.补充
四.重定向
1.重定向原理
2.dup2函数
五.Linux文件系统
1.初识inode
2.理解硬件磁盘
3.磁盘分区
4.Linux ext2文件系统
六.软硬链接
1.软连接
2.硬链接
3.软硬连接的对比
文件的三个时间
①任何进程在运行的时候都会默认打开三个输入输出流,即标准输入流、标准输出流以及标准错误流,对应到C语言当中就是stdin、stdout以及stderr。
②其中,标准输入流对应的设备就是键盘,标准输出流和标准错误流对应的设备都是显示器。
③查看man手册,stdin、stdout以及stderr这三个都是FILE*类型的
④当我们的C程序被运行起来时,操作系统就会默认打开三个输入输出流,之后我们才能调用类似于scanf和printf之类的函数向键盘和显示器进行相应的输入输出操作。
⑤stdin、stdout以及stderr与我们打开某一文件时获取到的文件指针指向的是同一个类型的结构体
注意:不止是C语言当中有标准输入流、标准输出流和标准错误流,C++当中也有对应的cin、cout和cerr,其他所有语言当中都有类似的概念。实际上这种特性并不是某种语言所特有的,而是由操作系统所支持的。
使用fopen以写入的方式打开一个文件时,若该文件不存在,则会自动在当前路径创建该文件,那么这里所说的当前路径指的是什么呢?
示例:
(1)代码
(2)第一次运行
(3)第二次运行
(4)结论:这里所说的当前路径不是指可执行程序所处的路径,而是指该可执行程序运行成为进程时我们所处的路径 ,即进程运行时所处的路径。
int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);
(1)第一个参数:pathname
open函数的第一个参数是pathname,表示要打开或创建的目标文件。
(2)第二个参数:flags
open函数的第二个参数是flags,表示打开文件的方式。
①常用选项: [ O_WRNOLY | O_CREAT = W (C语言) ]
参数选项 | 含义 |
---|---|
O_RDONLY | 以只读的方式打开文件 |
O_WRNOLY | 以只写的方式打开文件 |
O_APPEND | 以追加的方式打开文件 |
O_RDWR | 以读写的方式打开文件 |
O_CREAT | 当目标文件不存在时,创建文件 |
②系统接口open的第二个参数flags是整型,有32比特位,若将一个比特位作为一个标志位,则理论上flags可以传递32种不同的标志位,每一个选项只有一位为1.
查看系统中的定义: 每一个选项都是以宏的方式定义的
③将标志位分离,判断传入的flags的选项,底层再做下一步操作
int open(xxx, arg, xxx){
if (arg & O_RDONLY){
//设置O_RDONLY
}
if (arg & O_WRONLY){
//设置O_WRONLY
}
if (arg & O_CREAT){
//设置O_CREAT
}
......
}
(3)第三个参数: mode
open函数的第三个参数是mode,表示创建文件的默认权限,只在创建文件的时候有用。
(4)返回值int
①open函数的返回值是新打开文件的文件描述符fd
② 文件打开失败,返回-1、
③返回值的解释
(1)close
int close(int fd);
使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1;(文件描述符也是有限的,是一种资源,不使用要关闭,以便于给其他文件分配)
(2) write
ssize_t write(int fd, const void *buf, size_t count);
从buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。
(3)read
ssize_t read(int fd, void *buf, size_t count);
从文件描述符为fd的文件读取count字节的数据到buf位置当中。
(4)使用举例
1 #include //写文件
2 #include
3 #include
4 #include
5 #include
6 #include
7
8 int main()
9 {
10 int fd = open("log.txt" , O_WRONLY);
11 if(fd < 0){
12 perror("open error!\n");
13 return 1;
14 }
15
16 const char* buf = "file test ...\n";
17 int count = 5;
18 while(count--){
19 write(fd , buf , strlen(buf));
20 }
21
22 close(fd);
23
24 return 0;
1 #include //读文件
2 #include
3 #include
4 #include
5 #include
6 #include
7
8 int main()
9 {
10 int fd = open("log.txt" , O_RDONLY);
11 if(fd < 0){
12 perror("open error!\n");
13 return 1;
14 }
15
16 //法1;
17 // char buf[64];
18 // int count = 5;
19 // while(count--){
20 // read(fd , buf , strlen(buf));
21 // printf("%s" , buf);
22 // }
23
24 //法2;
25 char ch;
26 while(1){
27 ssize_t ret = read(fd ,&ch , 1);
28 printf("%c" , ch);
29
30 if(ret <= 0){
31 break;
32 }
33 }
34
35 close(fd);
36
37 return 0;
① 文件是由进程运行时打开的,一个进程可以打开多个文件,而系统当中又存在大量进程,也就是说,在系统中任何时刻都可能存在大量已经打开的文件。因此,操作系统务必要对这些已经打开的文件进行管理,操作系统会为每个已经打开的文件创建各自的struct file结构体,然后将这些结构体以双链表的形式连接起来,之后操作系统对文件的管理也就变成了对这张双链表的增删查改等操作。
②进程和文件建立联系
③内核对于files_struct 的描述
④什么叫做创建进程打开0,1,2
当某一进程创建时,操作系统就会根据键盘、显示器、显示器形成各自的struct file,将这3个struct file连入文件双链表当中,并将这3个struct file的地址分别填入fd_array数组下标为0、1、2的位置,至此就默认打开了标准输入流、标准输出流和标准错误流
⑤磁盘文件(程序)vs内存文件(进程)
⑥文件描述符的分配 : 从最小的,没有被使用的下标开始分配
⑦写的函数最终是在进程中调用,代码是要OS执行的。write 函数是进程调用,OS执行
当调用write函数时,OS知道当前进程的PCB找到文件指针数组,通过传入的fd找到管理该文件的数据结构。
(1)因为库函数是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的,所以C库当中的FILE结构体内部必定封装了文件描述符fd。
(2)在/usr/include/stdio.h
头文件中可以看到下面这句代码
typedef struct _IO_FILE FILE;
(3)查看 struct _IO_FILE内部定义
int _fileno成员,就是封装的文件描述符
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; //封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
(4)fopen做了什么
三种缓冲方式 :
(1)现象解释
①当我们直接执行可执行程序,将数据打印到显示器时所采用的就是行缓冲,因为代码当中每句话后面都有\n,所以当我们执行完对应代码后就立即将数据刷新到了显示器上。
②当我们将运行结果重定向到log.txt文件时,数据的刷新策略就变为了全缓冲,此时我们使用printf和fprintf函数打印的数据都打印到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,fork之后父子进程代码共享数据私有,由于进程间具有独立性,而之后当父进程或是子进程对要刷新缓冲区内容时(刷新之后数据没了,相当于写入了),本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,,父子进程各自有一份独立的数据,所以最终打印了两次。所以重定向到log.txt文件当中printf和fprintf函数打印的数据就有两份。
③write函数是系统接口,没有缓冲区,因此write函数打印的数据就只打印了一份。
(2)C语言缓冲区
①这个缓冲区是谁提供的? C语言,用户级概念
②这个缓冲区在哪里?
FILE结构体当中还有一部分成员是用于记录缓冲区相关的信息的。
//缓冲区相关
/* 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. */
(3)为什么文件是全缓冲,显示器是行缓冲?
计算机遵守冯诺依曼体系,文件本身在系统当中属于外设/外设之上,我们把数据刷新到磁盘上/显示器上都叫做往外设上面写,外设效率低,所以我们把数据积累到内存缓冲区里,积累足够多的时候刷新,这样的话就可以提高效率(送快递,一个一个送,一批送)
(4)全缓冲理论上效率最高,为什么显示器还要行刷新?
磁盘上的文件人在写的时候是不会读的,显示器不一样,当一个人往显示器上写数据时,这个人想要尽快拿到这个数据,不要缓冲效率就太低了,全缓冲人看到的消息就不及时,这样倒不如设置成行缓冲;本质行缓冲是在效率和可用性做的平衡。
(5)OS的缓冲区 : 不受缓冲方式的影响
操作系统实际上也是有缓冲区的,当我们刷新用户缓冲区的数据时,并不是直接将用户缓冲区的数据刷新到磁盘或是显示器上,而是先将数据刷新到操作系统缓冲区,然后再由操作系统在合适的时候将数据刷新到磁盘或是显示器上。(操作系统有自己的刷新机制)
因为操作系统是进行软硬件资源管理的软件,用户区的数据要刷新到外设必须经过操作系统,数据最终是由OS刷新到外设。
(1)数据刷新
①C语言的函数把数据写到用户级缓冲区全缓冲,如果close,把文件描述符1关了,最后进程退出的时候想刷到显示器但是fd 关了,没办法刷新到1了,只有在close前fflush强制刷新到内核缓冲区才能最终显示
②调用fcolse会先把用户级缓冲区的数据刷新到内核缓冲区,然后调用close关闭文件描述符。
③如果是长时间运行的程序文件使用完最好fclose,文件描述符有限,创建struct file需要消耗内存,防止内存泄漏;但是程序退出时会自动清空用户缓冲区,将数据刷新出来。
(2)stdout ,stderr都是将数据打印到显示器
(3)理解Linux下一切皆文件
①当上层要进行读写文件时,它根本不关心底层指向什么方法,只需要调用该函数指针指向的方法;所有的函数都不关心底层的差异,只需要调用read,write就完成了统—的读写,站在应用层就可以认为一切皆文件。
②所有的外设硬件,都有自己的read,write方法 ,绝对不一样。
(4)凡是显示到显示器上面的内容,都是字符; 凡是从键盘读取的内容都是字符 。所以,键盘和显示器一般被称之为“字符设备”
(1)输出重定向: 应该显示到显示器的内容显示到文件中,close(1),打开新文件时分配文件描述符1
代码+结果:
(2)追加重定向 : 只需要在open时添加O_APPEND字段
(3)输入重定向 : 应该从键盘中读取数据,close(0),新打开文件时分配文件描述符0,从文件中读取
代码+数据:
(1)函数介绍
int dup2(int oldfd, int newfd);
①函数功能: dup2会将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中 , newfd指向的文件不再使用时close(newfd) .
②函数返回值: dup2如果调用成功,返回newfd,否则返回-1。
③使用dup2时,我们需要注意以下两点:
④函数本质是修改下标空间里面指向file的地址,两个下标都指向同一个文件
(2)函数使用示例
(3)简易shell增加重定向功能
代码:
1 #include
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8 #include
9 #include
10
11 #define LEN 1024 //输入命令最大长度
12 #define NUM 32 //命令拆分后的最大个数
13
14 int main()
15 {
16
17 int type = 0;//0:> , 1:>> , 2:<
18 char cmd[LEN]; //存储命令
19 char* myargv[NUM]; //存储命令拆分后的结果
20 char hostname[32]; //主机名
21 char pwd[128]; //当前目录
22
23
24 while (1){
25 //获取命令提示信息
26 struct passwd* pass = getpwuid(getuid());
27 gethostname(hostname, sizeof(hostname)-1); //获取主机名
28 getcwd(pwd, sizeof(pwd)-1);//获取绝对地址
29 int len = strlen(pwd);
30 char* p = pwd + len - 1; //获取当前目录
31 while (*p != '/'){
32 p--;
33 }
34 p++;
35
36 //打印命令提示信息
37 printf("[%s@%s %s]$ ", pass->pw_name, hostname, p);
38
39 //从标准输入读取命令
40 fgets(cmd, LEN, stdin);
41 cmd[strlen(cmd) - 1] = '\0';
42
43
44 //实现重定向功能
45 char* start = cmd;
46 while (*start != '\0'){
47 if (*start == '>'){
48 type = 0; //遇到一个'>',输出重定向
49 *start = '\0';
50 start++;
51 if (*start == '>'){
52 type = 1; //遇到第二个'>',追加重定向
53 start++;
54 }
55 break;
56 }
57
58 if (*start == '<'){
59 type = 2; //遇到'<',输入重定向
60 *start = '\0';
61 start++;
62 break;
63 }
64 start++;
65 }
66
67 if (*start != '\0'){ //start位置不为'\0',说明命令包含重定向内容
68 while (isspace(*start)) //跳过重定向符号后面的空格
69 start++;
70 }
71 else{
72 start = NULL; //start设置为NULL,标识命令当中不含重定向内容
73 }
74
75
76 //拆分命令
77 myargv[0] = strtok(cmd, " ");
78 int i = 1;
79 while (myargv[i] = strtok(NULL, " ")){
80 i++;
81 }
82
83 pid_t id = fork(); //创建子进程执行命令
84 if (id == 0){
85 //child
86
87 if(start != NULL){
88 if(type == 0){ //输出重定向
89 int fd = open(start ,O_WRONLY | O_CREAT | O_TRUNC, 0664); //O_TRUNC清空内容
90 if(fd < 0){
91 perror("open error!\n");
92 exit(2);
93 }
94
95 close(1);
96 dup2(fd , 1);
97 }
98 else if(type == 1){ //追加重定向
99 int fd = open(start ,O_WRONLY | O_CREAT | O_APPEND , 0664);
100 if(fd < 0){
101 perror("open error!\n");
102 exit(2);
103 }
104
105 close(1);
106 dup2(fd , 1);
107 }
108 else if(type == 2){ //输入重定向
109 int fd = open(start , O_RDONLY);
110 if(fd < 0){
111 perror("open error!\n");
112 exit(2);
113 }
114
115 close(0);
116 dup2(fd , 0);
117
118 }
119 }
120
121 execvp(myargv[0], myargv); //进行程序替换
122 exit(1);
123 }
124
125 //父进程等待获取子进程的退出信息
126 int status = 0;
127 pid_t ret = waitpid(id, &status, 0);
128 if (ret > 0){
129 printf("exit code:%d\n", WEXITSTATUS(status));
130 }
131 }
132 return 0;
133 }
结果:
(1)查看文件的inode
(2)Linux把文件的属性和内容进行分离存储
文件 = 内容 + 属性 , 属性又叫做元信息,在inode中保存
(3)inode是任何一个文件的属性集合,linux中几乎每一个文件都有一个inode ; 可能存在大量的inode,区分inode,用inode编号
(4) ls看属性 , cat看内容
(1)什么是磁盘?
磁盘是一种永久性存储介质,在计算机中,磁盘几乎是唯一的机械设备。与磁盘相对应的就是内存,内存是掉电易失存储介质,目前所有的普通文件都是在磁盘中存储的。
(2)磁盘基本概念
(3)磁盘如何找到数据
(1)理解文件系统,首先我们必须将磁盘想象成一个线性的存储介质,那么对硬盘的管理就转化为对数组的管理.
(2)磁盘通常被称为块设备,磁盘访问的最小单位为扇区,一个扇区的大小通常为512字节。
(3)磁盘分区理解
①为什么要分区? 划分区域,为了更好的管理(中国有很多的省)
②只要能把一个区域管好,剩下的区域复制黏贴式,用相同的方法管理
③分区1,2,3 就像Windows的 C , D ,E盘
(4)磁盘格式化
磁盘格式化就是对磁盘中的分区进行初始化的一种操作,这种操作通常会导致现有的磁盘或分区中所有的文件被清除;磁盘格式化就是对分区后的各个区域写入对应的管理信息 , 写入的管理信息是什么是由文件系统决定的,不同的文件系统格式化时写入的管理信息是不同的,常见的文件系统有EXT2、EXT3、XFS、NTFS等。
(1)为了更好的管理磁盘,会对磁盘每一个分区再次划分
注意: 启动块的大小是确定的,而块组的大小是由格式化的时候确定的,并且不可以更改。
(2)而每个Block Group都有着相同的结构组成 ,块组里面也划分了很多区域
①其他块组当中可能会存在冗余的Super Block,当某一Super Block被破坏后可以通过其他Super Block进行恢复。
②磁盘分区并格式化后,每个分区的inode个数就确定了。
③比特位的位置 : 哪一个inode ; 比特位的内容 : 是否被占用(0,1)
(3)更多理解
①如何理解目录?
②如何理解ls 和 ls -l 和cat
③再次理解目录权限
不管是对目录下文件的读还是写,其他文件是存在目录文件的数据区,只有目录有x权限才能访问目录文件的数据。
④如何理解创建文件
⑤如何理解对文件写入
⑥关于inode结构中如何记录数据块
一个文件使用的数据块和inode结构的对应关系,是通过一个数组进行维护的,该数组一般可以存储15个元素,其中前12个元素分别对应该文件使用的12个数据块,剩余的三个元素分别是一级索引、二级索引和三级索引,当该文件使用数据块的个数超过12个时,可以用这三个索引进行数据块扩充
⑦如何理解删除文件
⑧如何理解复制文件慢 , 删除文件快
因为拷贝文件需要先创建文件,然后再对该文件进行写入操作,该过程需要先申请inode号并填入文件的属性信息,之后还需要再申请数据块号,最后才能进行文件内容的数据拷贝,而删除文件只需将对应文件的inode号和数据块号置为无效即可,无需真正的删除文件,因此拷贝文件是很慢的,而删除文件是很快的。
⑨面试:为什么我的磁盘还有空间,却不能创建文件?
一个磁盘的inode在初始化的时候已经设定好了,inode用完了,无法再创建文件。
(1)创建软连接
[gsx@VM-0-2-centos 220618]$ ln -s test test-s
(2)软链接文件的inode号与源文件的inode号是不同的,并且软链接文件的大小比源文件的大小要小得多。
(3)软链接又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件,该文件有自己的inode号,但是该文件只包含了源文件的路径名,所以软链接文件的大小要比源文件小得多。软链接就类似于Windows操作系统当中的快捷方式。
(1)创建硬链接
[gsx@VM-0-2-centos 220618]$ ln test test-h
(2)硬链接文件的inode号与源文件的inode号是相同的,并且硬链接文件的大小与源文件的大小也是相同的,特别注意的是,当创建了一个硬链接文件后,该硬链接文件和源文件的硬链接数都变成了2。
(3) 硬链接原理
(4)新创建的目录默认硬连接数是2
因为每个目录创建后,该目录下默认会有两个隐含文件.和..,它们分别代表当前目录和上级目录,因此这里创建的目录有两个名字,一个是dir另一个就是该目录下的.,所以刚创建的目录硬链接数是2。
(1)软链接是一个独立的文件,有独立的inode,而硬链接没有独立的inode。
(2)软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录.
(3)硬链接用处:方便目录之间通过相对路径方式进行跳转
在Linux当中,使用命令stat 文件名来查看对应文件的信息。
三个时间:
①当我们修改文件内容时,文件的大小一般也会随之改变,所以一般情况下Modify的改变会带动Change一起改变,但修改文件属性一般不会影响到文件内容,所以一般情况下Change的改变不会带动Modify的改变。
②touch一个已经存在的文件只会更改该文件的时间信息,三个时间都会改变