预备知识
文件=内容+属性
1 所有对文件的操作可分为两类:a 对内容操作 b 对属性操作
2 内容是数据,属性也是数据,存储文件必须既要存储内容,也要存储属性数据
默认文件在磁盘上
3 进程访问一个文件的时候,都要先把这个文件打开
打开前:普通的磁盘文件
打开后:将文件加载到内存-->加载磁盘上的文件,一定会涉及到访问磁盘设备-->操作系统来做
4 一个进程可以打开多个文件,加载到内存中,被打开的文件可能会存在多个
进程:打开的文件 = 1:n(1)
结论:操作系统在运行中,可能会打开多个文件,操作系统需要管理这些打开的文件-->先描述再组织-->一个文件被打开,一定要先在内核中形成数据结构对象
5 文件按照是否被打开,分为:被打开的文件(在内存中)和没有被打开的文件(在磁盘中)
6 研究文件操作的本质:进程和被打开文件的关系
常见的c的文件接口
1 #include<stdio.h>
2
3 int main()
4 {
5 //"w": 按照写方式打开,如果文件不存在就创建它。
6 FILE *fp = fopen("log.txt", "w");
7 if(NULL==fp)
8 {
9 perror("fopen");
10 return 1;
11 }
12
13 const char *msg = "hello\n";
14 int cnt = 10;
15 while(cnt)//向文件中写入10行hello
16 {
17 fputs(msg,fp);
18 cnt--;
19 }
20 fclose(fp);
21 return 0;
22 }
将以上代码稍作修改:
1 #include<stdio.h>
2
3 int main()
4 {
5 //"w": 按照写方式打开,如果文件不存在就创建它。
6 FILE *fp = fopen("log.txt", "w");
7 if(NULL==fp)
8 {
9 perror("fopen");
10 return 1;
11 }
12
13 const char*msg = "aaaaaaaaaaaaaaaaa\n";
14 fputs(msg,fp);
15 fclose(fp);
16 return 0;
17 }
结论:文件以‘w’方式打开,会先清空文件内容
>文件名:可以清空该文件的内容(输出重定向,以w方式打开文件)
1 #include<stdio.h>
2
3 int main()
4 {
5 //"a"方式打开文件,直接在已有内容后追加
6 FILE *fp = fopen("log.txt", "a");
7 if(NULL==fp)
8 {
9 perror("fopen");
10 return 1;
11 }
12
13 const char*msg = "hello\n";
14 fputs(msg,fp);
15 fclose(fp);
16 return 0;
17 }
a:也是写入,从文件结尾处开始写入,即“追加”,不清空文件内容
>>文件名:追加重定向,打开文件以a方式打开
一个进程通过操作系统打开文件,那么操作系统一定要给我们提供系统调用接口
我们学习的c语言打开文件的接口,底层一定封装了系统调用接口
系统文件I/O
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问
open与close:
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);
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:-1
两个open的使用场景:目标文件不存在,需要使用三个参数的open,第三个参数表示创建文件
的默认权限,否则,使用两个参数的open
open函数的返回值是文件描述符fd
close:
函数传入标志位的技巧--linux中常用的传参方式 :位图方式
#include <stdio.h>
#define Print1 1 // 0001
#define Print2 (1<<1) // 0010
#define Print3 (1<<2) // 0100
#define Print4 (1<<3) // 1000
void Print(int flags)
{
if(flags&Print1) printf("hello 1\n");
if(flags&Print2) printf("hello 2\n");
if(flags&Print3) printf("hello 3\n");
if(flags&Print4) printf("hello 4\n");
}
int main()
{
Print(Print1);
Print(Print1|Print2);
Print(Print1|Print2|Print3);
Print(Print3|Print4);
Print(Print4);
return 0;
}
6 int main()
7 {
8 int fd = open("log.txt", O_WRONLY | O_CREAT);
9 if(fd<0)
10 {
11 perror("open");
12 return 1;
13 }
14 close(fd);
15 return 0;
16 }
创建文件时我们没有指明以何种权限去创建,所以log.txt创建出来后会如上图一般
open的第三个参数就是权限的意思
上面代码稍作修改:
我们虽然设置的权限为666,但是由于系统默认设置的权限掩码为0002,所以最终该文件的权限为664
umask:
当然了,如果你一定想要按照自己设置的权限创建文件,那么可以先将权限掩码设置为0:umask(0)
系统调用 和 库函数
write:
fd:向哪一个文件进行写入
buf:要写入字符串的起始地址
count:写入的字节数
返回值:实际写入多少
当我们想向一个文件中写入字符串的时候,求其长度不需要在strlen()后在加上1,因为'\0'是c语言的规定,不是文件的规定
7 int main()
8 {
9 int fd = open("log.txt", O_WRONLY | O_CREAT,0666);
10 if(fd<0)
11 {
12 perror("open");
13 return 1;
14 }
15
16 const char*msg = "hello\n";
17 write(fd,msg,strlen(msg));
18 close(fd);
19 return 0;
20 }
当我们再向文件log.txt中写入aaa时:
如果我们想要使用open以写方式打开文件时,先把文件内容清空,则需要 O_TRUNC
再改进以上代码:
O_WRONLY | O_CREAT | O_TRUNC:按照写方式打开,如果文件不存在就创建它,会先清空文件内容!
O_WRONLY | O_CREAT | O_APPEND:按照写方式打开,如果文件不存在就创建它,从文件结尾处开始写人-->“追加”,不清空文件
7 int main()
8 {
9 int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND,0666);
10 if(fd<0)
11 {
12 perror("open");
13 return 1;
14 }
15
16 const char*msg = "aaa\n";
17 write(fd,msg,strlen(msg));
18 close(fd);
19 return 0;
20 }
FILE是一个C语言提供的结构体类型,其必定封装了文件描述符
(操作系统访问文件,只认文件描述符),stdin,stdout,stderr都是FILE*类型
下面证明一下:
printf("stdin->fd: %d\n", stdin->_fileno);
printf("stdout->fd: %d\n", stdout->_fileno);
printf("stderr->fd: %d\n", stderr->_fileno);
FILE *fp = fopen("log.txt", "w");
printf("fp->fd: %d\n", fp->_fileno);
fclose(fp);
文件描述符fd
关于open的返回值fd:
我们先打印看一下
7 int main()
8 {
9 int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND,0666);
10 if(fd<0)
11 {
12 perror("open");
13 return 1;
14 }
15
16 printf("fd: %d\n",fd);
17
18 const char*msg = "aaa\n";
19 write(fd,msg,strlen(msg));
20 close(fd);
21 return 0;
22 }
7 int main()
8 {
9 int fda = open("loga.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
10 int fdb = open("logb.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
11 int fdc = open("logc.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
12 int fdd = open("logd.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
13
14 printf("fda: %d\n", fda);
15 printf("fdb: %d\n", fdb);
16 printf("fdc: %d\n", fdc);
17 printf("fdd: %d\n", fdd);
18
19 close(fda);
20 close(fdb);
21 close(fdc);
22 close(fdd);
23 return 0;
24 }
现象:fd是一个连续的小整数
为什么没有0,1,2?
理解文件在操作系统中的表现:
文件描述符fd的本质就是数组的下标
操作系统访问文件,只认文件描述符
linux的部分源代码:
0 & 1 & 2
现在来解答以下为什么我们使用的fd没有0,1,2:
进程在运行时,默认会把三个文件打开(linux下一切皆文件):
标准输入--键盘--stdin--0
标准输出--显示器--stdout--1
标准错误--显示器--stderr--2
OS/C语言默认把0,1,2,stdin,stdout,stderr打开,就是为了让程序员默认进行输入输出代码编写
理解struct file内核对象
无论读写都要先把数据加载到文件缓冲区
我们在应用层进行数据的读写,本质是将内核缓冲区中的数据进行来回拷贝
以下是草图:
用户层面也会有一个缓冲区,读:会将内核中文件缓冲区内的数据拷贝到用户层面的缓冲区
写:会将用户层面缓冲区内的数据写到文件缓冲区,再刷新出去
linux的部分源代码:
文件描述符fd的分配规则
先看一段代码:
8 #define FILE_NAME "log.txt"
9
10 int main()
11 {
12 int fd = open(FILE_NAME,O_WRONLY|O_CREAT|O_TRUNC,0666);
13 if(fd<0)
14 {
15 perror("open");
16 return 1;
17 }
18
19 printf("fd: %d\n",fd);
20 close(fd);
21 return 0;
22 }
read:
10 int main()
11 {
12 char buffer[1024];
13 ssize_t s = read(0, buffer, 1024);//读取文件描述符为0的文件(键盘)中的内容
14 if(s > 0)
15 {
16 buffer[s-1] = 0;//去掉最后一定会输入进来的'\n'
17 printf("echo# %s\n", buffer);
18 }
19 return 0;
20 }
write:
10 int main()
11 {
12 char buffer[1024];
13 ssize_t s = read(0, buffer, 1024);
14 if(s > 0)
15 {
16 buffer[s-1] = 0;//去掉最后一定会输入进来的'\n'
17 write(1, buffer, strlen(buffer));//向显示器写入buffer
18 }
19 return 0;
20 }
结论1:进程默认已经打开了0,1,2,我们可以直接使用0,1,2进行数据的访问
关闭0号文件描述符:
8 #define FILE_NAME "log.txt"
9
10 int main()
11 {
12 close(0);
13 int fd = open(FILE_NAME, O_CREAT|O_WRONLY|O_TRUNC, 0666);
14 if(fd < 0)
15 {
16 perror("open");
17 return 1;
18 }
19
20 printf("fd: %d\n",fd);
21 close(fd);
22 return 0;
23 }
关闭2号文件描述符:
结论2:文件描述符的分配规则为:寻找最小的,没有被使用的数组位置,分配给指定的打开文件
关闭1号文件描述符:
8 #define FILE_NAME "log.txt"
9
10 int main()
11 {
12 close(1);
13 int fd = open(FILE_NAME, O_CREAT|O_WRONLY|O_TRUNC, 0666);
14 if(fd < 0)
15 {
16 perror("open");
17 return 1;
18 }
19
20 printf("fd: %d\n",fd);
21 printf("stdout->fd: %d\n",stdout->_fileno);
22 fflush(stdout);
23 close(fd);
24 return 0;
25 }
原本两条printf语句要向显示器上打印,但是打印到了log.txt中,这种现象叫做输出重定向
将printf换成fprintf:
将fflush这一行注释掉:
这是因为用户有自己的缓冲区,C语言也有一个缓冲区,我们向文件写入数据时,会先将用户自己缓冲区里的内容写入到C语言的缓冲区,数据会先暂存在这里,因为后文代码有一条:close(fd)语句,如果不强制刷新该缓冲区,让数据写入文件中,那么一旦close(fd)执行,直接关闭文件,那么就无法向文件中写入数据了
8 #define FILE_NAME "log.txt"
9
10 int main()
11 {
12 close(0);
13 int fd = open(FILE_NAME, O_RDONLY);
14 if(fd < 0)
15 {
16 perror("open");
17 return 1;
18 }
19
20 char buffer[1024];
//从buffer读取数据
21 fread(buffer,1,sizeof(buffer),stdin);//stdin->fd:0 //输入重定向
22 printf("%s\n",buffer);
23 close(fd);
24 return 0;
25 }
重定向
常见的重定向有:>, >>, <
重定向的基本原理:
上层fd不变,底层fd指向的内容在改变
我们通常是不愿意先关闭0,1,2号文件描述符,再打开文件,通过文件描述符分配规则来完成重定向的,所以更简单的重定向是:
文件描述表级别的数组内容的拷贝 ,使用dup2
未输出重定向之前:
8 #define FILE_NAME "log.txt"
9
10 int main()
11 {
12 int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
13 if(fd<0)
14 {
15 perror("open");
16 return 1;
17 }
18
19 printf("hello printf\n");
20 fprintf(stdout,"hello fprintf\n");
21 close(fd);
22 return 0;
23 }
输出重定向之后:
8 #define FILE_NAME "log.txt"
9
10 int main()
11 {
12 int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
13 if(fd<0)
14 {
15 perror("open");
16 return 1;
17 }
18
19 dup2(fd,1);
20
21 printf("hello printf\n");
22 fprintf(stdout,"hello fprintf\n");
23 close(fd);
24 return 0;
25 }
追加重定向:
8 #define FILE_NAME "log.txt"
9
10 int main()
11 {
12 int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
13 if(fd<0)
14 {
15 perror("open");
16 return 1;
17 }
18
19 dup2(fd,1);
20
21 printf("hello printf\n");
22 fprintf(stdout,"hello fprintf\n");
23 close(fd);
24 return 0;
25 }
输入重定向:
8 #define FILE_NAME "log.txt"
9
10 int main()
11 {
12 int fd = open(FILE_NAME, O_RDONLY);
13 if(fd<0)
14 {
15 perror("open");
16 return 1;
17 }
18
19 dup2(fd,0);
20
21 char buffer[1024];
22 fread(buffer,1,1024,stdin);
23 printf("%s",buffer);
24 close(fd);
25 return 0;
26 }
上篇博客中我写过简易的shell代码,今日完善下它的重定向功能
程序替换不会影响重定向
myshell.c:
12 #define NoneRedir 0//无重定向
13 #define OutputRedir 1//输出重定向
14 #define AppendRedir 2//追加重定向
15 #define InputRedir 3//输入重定向
16
17 int redir = NoneRedir;
18 char *filename = NULL;
19
20 #define NUM 1024
21 #define SIZE 64
22 #define SEP " "
23 //#define Debug 1
24
25 int lastcode = 0;//最近一个进程的退出码
26 char enval[1024];//设置环境变量
27 char cwd[1024];//设置环境变量PWD的值
28
29 const char *getUsername()
30 {
31 const char *name = getenv("USER");
32 if(name) return name;
33 else return "none";
34 }
35 const char *getHostname()
36 {
37 const char *hostname = getenv("HOSTNAME");
38 if(hostname) return hostname;
39 else return "none";
40 }
41 const char *getCwd()
42 {
43 const char *cwd = getenv("PWD");
44 if(cwd) return cwd;
45 else return "none";
46 }
47
48 int getUserCommand(char *command, int num)
49 {
50 printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
51 char *r = fgets(command, num, stdin); // 最终你还是会输入\n
52 if(r == NULL) return -1;
53 // "abcd\n" 处理'\n'
54 command[strlen(command) - 1] = '\0'; //不会越界,至少会有\n
55 return strlen(command);
56 }
57
58 void commandSplit(char *in, char *out[])
59 {
60 int argc = 0;
61 out[argc++] = strtok(in, SEP);
62 while( out[argc++] = strtok(NULL, SEP));
63
64 #ifdef Debug
65 for(int i = 0; out[i]; i++)
66 {
67 printf("%d:%s\n", i, out[i]);
68 }
69 #endif
70 }
71
72 int execute(char *argv[])
73 {
74 pid_t id = fork();
75 if(id < 0) return -1;
76 else if(id == 0) //child
77 {
78 int fd = 0;
79 if(redir == InputRedir)
80 {
81 fd = open(filename, O_RDONLY);
82 dup2(fd, 0);
83 }
84 else if(redir == OutputRedir)
85 {
86 fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
87 dup2(fd, 1);
88 }
89 else if(redir == AppendRedir)
90 {
91 fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
92 dup2(fd, 1);
93 }
94 else
95 {
96 //do nothing
97 }
98 // exec command
99 execvp(argv[0], argv); // cd ..
100 exit(1);
101 }
102 else // father
103 {
104 int status = 0;
105 pid_t rid = waitpid(id, &status, 0);
106 if(rid > 0){
107 lastcode = WEXITSTATUS(status);
108 }
109 }
110
111 return 0;
112 }
113
114
115 char *homepath()
116 {
117 char *home = getenv("HOME");
118 if(home) return home;
119 else return (char*)".";
120 }
121
122
123 void cd(const char *path)
124 {
125 chdir(path);
126 char tmp[1024];
127 getcwd(tmp, sizeof(tmp));
128 sprintf(cwd, "PWD=%s", tmp);
129 putenv(cwd);
130 }
131
132 //bash自己执行内建命令,类似于bash内部的一个函数
133 int doBuildin(char *argv[])
134 {
135 if(strcmp(argv[0], "cd") == 0)
136 {
137 char *path = NULL;
138 if(argv[1] == NULL) path=homepath();
139 else path = argv[1];
140 cd(path);
141 return 1;
142 }
143 else if(strcmp(argv[0], "export") == 0)
144 {
145 if(argv[1] == NULL) return 1;
146 strcpy(enval, argv[1]);
147 putenv(enval);
148 return 1;
149 }
150 else if(strcmp(argv[0], "echo") == 0)
151 {
152 if(argv[1] == NULL)//为空
153 {
154 printf("\n");
155 return 1;
156 }
157 if(*(argv[1]) == '$' && strlen(argv[1]) > 1)//不为空,且紧跟$
158 {
159 char *val = argv[1]+1; //1. $PATH(环境变量) $?(退出码)
160 if(strcmp(val, "?") == 0)
161 {
162 printf("%d\n", lastcode);
163 lastcode = 0;
164 }
165 else
166 {
167 const char *enval = getenv(val);
168 if(enval) printf("%s\n", enval);
169 else printf("\n");
170 }
171 return 1;
172 }
173 else
174 {
175 printf("%s\n", argv[1]);
176 return 1;
177 }
178 }
179 //.......其他内建命令
180
181 return 0;
182 }
183
184 #define SkipSpace(pos) do{ while(isspace(*pos)) pos++; }while(0)
185
186 void checkRedir(char usercommand[], int len)
187 {
188 // ls -a -l > log.txt
189 // ls -a -l >> log.txt
190 char *end = usercommand + len - 1;
191 char *start = usercommand;
192 while(end>start)
193 {
194 if(*end == '>')
195 {
196 if(*(end-1) == '>')
197 {
198 *(end-1) = '\0';
199 filename = end+1;
200 SkipSpace(filename);
201 redir = AppendRedir;
202 break;
203 }
204 else
205 {
206 *end = '\0';
207 filename = end+1;
208 SkipSpace(filename);
209 redir = OutputRedir;
210 break;
211 }
212 }
213 else if(*end == '<')
214 {
215 *end = '\0';
216 filename = end+1;
217 SkipSpace(filename); // 跳过字符串前的空格
218 redir = InputRedir;
219 break;
220 }
221 else
222 {
223 end--;
224 }
225 }
226 }
227
228 int main()
229 {
230 while(1)
231 {
232 redir = NoneRedir;
233 filename = NULL;
234 // 1. 打印提示符&&获取用户命令字符串获取成功
235 char usercommand[NUM];
236 int n = getUserCommand(usercommand, sizeof(usercommand));
237 if(n <= 0) continue;
238
239 // "ls -a -l > log.txt" :检测是否发生了重定向
240 checkRedir(usercommand, strlen(usercommand));
241 // 2. 分割字符串
242 // "ls -a -l" -> "ls" "-a" "-l"
243 char *argv[SIZE];
244 commandSplit(usercommand, argv);
245 // 3. check build-in command 检查并让mybash执行内建命令
246 n = doBuildin(argv);
247 if(n) continue;
248 // 4. 子进程执行对应的命令
249 execute(argv);
250 }
251 }
文件描述符0,1,2都是默认打开的,前两者很正常,但是为什么要有2?即为什么要有标准错误?尤其是它指向的也是显示器
fprintf(stdout,"hello stdout\n");
fprintf(stderr,"hello stderr\n");
因为重定向的时候只是,文件描述符表的数组下标为1的内容被log.txt的3号文件描述符里的内容给拷贝替换了,1号(stdout)已经指向log.txt了,与2号没有关系,2号(stderr)还是指向显示器
要让它两都打印到同一个文件里,只需要加上 2>&1:将1号文件描述符这个数组下标所对应的内容拷贝到2号文件描述符这个数组下标的内容里
打印信息有常规信息,有错误信息,默认打开2号文件描述符,那么我们就可以利用上图操作将常规信息与错误信息分开在不同文件里,方便排查错误
缓冲区
我们理解的缓冲区是一部分内存,形象理解缓存区,可以将其视作菜鸟驿站等快递点
缓冲区的主要作用是提高使用者的效率,因为有缓冲区的存在,我们可以积累一部分再统一发送。由此提高发送的效率
缓冲区因为能够暂存数据,必定要有一定的刷新方式:
1 无缓冲(立即刷新)
2 行缓冲(行刷新)
3 全缓冲(缓冲区满了,再刷新)
以上都是刷新的一般策略,但会有特殊情况:
1 强制刷新
2 进程退出的时候,一般要进行刷新缓冲区
一般对于显示器文件(linux下一切皆文件):行刷新(行缓冲)
对于磁盘上的文件:全缓冲(缓冲区写满再刷新)
一个样例:
10 int main()
11 {
//都向显示器打印
12 fprintf(stdout, "C: hello fprintf\n");
13 printf("C: hello printf\n");
14 fputs("C: hello fputs\n", stdout);
15 const char *str = "system call: hello write\n";
16 write(1, str, strlen(str));
17 return 0;
18 }
向以上代码再添加一条fork语句:
10 int main()
11 {
12 fprintf(stdout, "C: hello fprintf\n");
13 printf("C: hello printf\n");
14 fputs("C: hello fputs\n", stdout);
15 const char *str = "system call: hello write\n";
16 write(1, str, strlen(str));
17
18 fork();//注意fork的位置
19 return 0;
20 }
输出重定向到log.txt后,只有系统调用接口打印了一次,库函数都打印了两次
理解该样例:
1 当我们直接向显示器打印的时候,显示器文件的刷新方式是行刷新,且我们的代码输出的所有字符串,都有'\n',在fork之前,数据全部已经被刷新
2 重定向到log.txt,本质是向磁盘文件中写入(注意:不是显示器了!),我们系统对于数据的刷新方式,已经由行刷新变成了全缓冲
3 全缓冲意味着缓冲区变大,实际写入的简单数据,不足以把缓冲区写满,fork执行的时候,数据依旧在缓冲区中
4 我们目前所谈的“缓冲区”,和操作系统是没有关系的,只能和C语言本身有关
因为系统调用接口write不管是向显示器还是磁盘文件上打印,都只打印一次,fprintf,printf,fputs这些c语言提供的函数,底层都封装了write,write本身只打印一次,所以症结所在是C语言
我们日常用的用的最多的其实是C/C++提供的语言级别的缓冲区
5 C/C++提供的缓冲区,里面保存的一定是用户的数据,属于当前进程在运行时自己的数据,如果我们把数据交给了OS,这个数据就属于OS,不属于你了
6 当进程退出的时候,一般要进行刷新缓冲区(属于清空或者写入操作),即使数据没有满足刷新条件
重定向到log.txt中,数据仍然在缓冲区中,还是属于进程的,fork创建子进程,父子进程默认数据共享,任意一个进程在退出时,刷新缓冲区(属于清空或者写入操作)就要发生写时拷贝,数据有两份,所以会打印两次
write系统调用,没有使用C的缓冲区,而是直接把数据写入到操作系统,那么数据就不属于进程了
自然不会发生写时拷贝
刷新
将C缓冲区的数据写入到OS,就叫刷新
示意草图
用户缓冲区实际就是C语言提供的缓冲区,内核缓冲区就是文件缓冲区
C语言提供的缓冲区在哪里?
答案:任何情况下,我们输入输出的时候,都要有一个FILE,FILE是一个结构体,里面包含了fd,也提供一段缓冲区
模拟实现C标准版库的函数--只做代码说明
简易实现
mystdio.h:
#pragma once
#define SIZE 4096
#define FLUSH_NONE 1
#define FLUSH_LINE (1<<1)
#define FLUSH_ALL (1<<2)
typedef struct _myFILE
{
int fileno;
int flag;
char buffer[SIZE];
int end;
}myFILE;
extern myFILE *my_fopen(const char *path, const char *mode);
extern int my_fwrite(const char *s, int num, myFILE *stream);
extern int my_fflush(myFILE *stream);
extern int my_fclose(myFILE*stream);
mystdio.c:
10 #define DFL_MODE 0666
11
12 myFILE *my_fopen(const char *path, const char *mode)
13 {
14 int fd = 0;
15 int flag = 0;
16 if(strcmp(mode, "r") == 0)
17 {
18 flag |= O_RDONLY;
19 }
20 else if(strcmp(mode, "w") == 0)
21 {
22 flag |= (O_CREAT | O_TRUNC | O_WRONLY);
23 }
24 else if(strcmp(mode, "a") == 0)
25 {
26 flag |= (O_CREAT | O_WRONLY | O_APPEND);
27 }
28 //.....
29
30 if(flag & O_CREAT)
31 {
32 fd = open(path, flag, DFL_MODE);
33 }
34 else
35 {
36 fd = open(path, flag);
37 }
38
39 if(fd < 0)
40 {
41 errno = 2;
42 return NULL;
43 }
44 myFILE *fp = (myFILE*)malloc(sizeof(myFILE));
45 if(!fp)
46 {
47 errno = 3;
48 return NULL;
49 }
50 fp->flag = FLUSH_LINE;
51 fp->end = 0;
52 fp->fileno = fd;
53 return fp;
54 }
55
56
57 int my_fwrite(const char *s, int num, myFILE *stream)
58 {
59 //写入到缓冲区,即FILE中的缓冲区buffer里
60 memcpy(stream->buffer+stream->end, s, num);
61 stream->end += num;
62
63 // 判断是否需要刷新,即若该字符串带\n,简单代码,暂不考虑此种情况: "abcd\nefg"
64 //刷新方式为行缓冲+缓冲区有数据+字符串以'\n'结尾:立即刷新
65 if((stream->flag & FLUSH_LINE) && stream->end > 0 && stream->buffer[stream->end-1] == '\n')
66 {
67 my_fflush(stream);
68 }
69
70 return num;
71 }
72
73
74
75 int my_fflush(myFILE *stream)
76 {
77 //刷新:将缓冲区的数据写入到OS里
78 if(stream->end > 0)
79 {
80 write(stream->fileno, stream->buffer, stream->end);
81 stream->end = 0;
82 }
83 return 0;
84 }
85
86
87 int my_fclose(myFILE*stream)
88 {
89 my_fflush(stream);
90 return close(stream->fileno);
91 }
main.c:
1:
7 int main()
8 {
9 myFILE *fp = my_fopen("./log.txt", "w");
10 if(fp == NULL)
11 {
12 perror("my_fopen");
13 return 1;
14 }
15
16 int cnt = 5;
17 const char *msg = "hello hello\n";
18 while(cnt--)
19 {
20 my_fwrite(msg, strlen(msg), fp);
21 sleep(1);
22 }
23
24 my_fclose(fp);
25 return 0;
26 }
2
7 int main()
8 {
9 myFILE *fp = my_fopen("./log.txt", "w");
10 if(fp == NULL)
11 {
12 perror("my_fopen");
13 return 1;
14 }
15
16 int cnt = 5;
17 const char *msg = "hello hello";//不添加\n
18 while(cnt--)
19 {
20 my_fwrite(msg, strlen(msg), fp);
21 sleep(1);
22 }
23
24 my_fclose(fp);
25 return 0;
26 }
不是所有的文件都会被打开,大部分文件都不是被打开的(当前并不需要被访问)都在磁盘中进行保存
没有被打开的文件在磁盘中存放也是需要被管理的,对于这部分文件的核心工作:快速定位文件,管理这些没有被打开的文件叫做文件系统
文件系统其实就是关于文件存储的问题,方便用户/OS进行增删查改
理解文件系统
我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据
每行包含7列:
模式 硬链接数 文件所有者 组 大小 最后修改时间 文件名
ls -l读取存储在磁盘上的文件信息,然后显示出来
inode
为了能解释清楚inode我们先简单了解一下文件系统
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被 划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改,上图中启动块(Boot Block)的大小是确定的
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相 同的结构组成。
超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的 时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个 文件系统结构就被破坏了
GDT,Group Descriptor Table:块组描述符,描述块组属性信息
块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用
i节点表:存放文件属性 如 文件大小,所有者,最近修改时间等
数据区:存放文件内容
磁盘
磁盘的物理存储结构
我们所有的文件存储的问题,本质是文件的属性与内容的存储,最终都会是二进制数据,在磁盘中以扇区为基本单元保存好
磁头会左右摆动以寻找柱面/磁道,寻找成功后悬浮不动,盘面旋转将某个扇区转到磁头底下来完成后续工作,简单来说:磁头摆动定位柱面,盘面旋转定位扇区,这样就可以找到某一个读写位置
磁盘存储的逻辑抽象结构
操作系统,可以按照扇区为单位进行存取,也可以基于文件系统,按照文件块为单位进行数据存取
用户只用文件名,内核只用inode编号,所以必须将文件名与其inode编号构成一个映射关系
目录也是文件,目录的内容存储自己内部直接保存的文件的文件名与inode映射关系:test.c:1234
所以目录下不允许同名文件的存在
linux中文件名不属于文件属性
访问文件的时候,最开始是要搞清楚该文件在哪一个分区?
思路:一个磁盘被分区格式化之后,Linux中要使用这个分区,那么要把这个分区进行挂载mount
查看linux分区挂载的情况:
df -h
答案:每一个文件都有路径,可以通过路径的前缀判断,判断出该路径在哪一个分区下
软硬链接
用后者来链接前者
结论:软链接是一个独立的文件,硬链接不是,因为它没有独立的inode编号
软链接
什么是软链接?
举个栗子:日常生活中我们会按需安装软件,一般都会在D盘/C盘安装,它的路径可能会很深,当我们要运行某个软件时,如果真的去D盘/C盘找到它,打开它,那就比较繁琐,但是快捷方式的出现可以解决问题,软链接差不多就是快捷方式了
写一个示例代码,用于接下来的演示:
1 #include<stdio.h>
2
3 int main()
4 {
5 printf("hello\n");
6 printf("hello\n");
7 printf("hello\n");
8 return 0;
9 }
是不是稍显麻烦呢?这时候软链接就可以上场了
所以如果某些我们要运行的程序藏得比较深,路径很长,我们想要直接在外部直接就可以运行,那么就建立软链接,来方便我们快速定位到该程序
软链接类似于windows得快捷方式,它是独立文件,有独立的inode,软链接内容是指向的目标文件的路径
硬链接
硬链接不是一个独立文件,它就是:在指定目录内部的一组映射关系,即文件名<->inode的映射关系
一个文件什么时候真正被删除?
答案:没有文件<->inode的映射时(即没有人用了)
在文件系统层面,目标文件怎么知道没有文件名指向它了呢?
答案:inode内部有引用计数,表示文件名映射关系的个数
文件名在目录里具有唯一性,和“指针”相似
为什么新建一个普通文件,它的硬链接数是1,而新建一个目录, 它的硬链接数是2呢?
普通文件只有一个 文件名与其inode对应,但是newdir虽然是个新建的空目录,但是会包含隐藏文件
表示当前目录的".",对应的inode正与newdir对应的inode一致,所以硬链接数为2
现象:newdir的硬链接数由2变成了3
这是因为dir里面的隐藏文件“.."表示上级目录,即newdir,它对应的inode编号与newdir一样
因此硬链接数在2的基础上+1
用户无法对目录建立硬链接,但是有特殊情况:"." ".."当前目录和上级目录可以
动态库与静态库
静态库与动态库
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文 件的整个机器码
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚 拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间
库搜索路径
从左到右搜索-L指定的目录
由环境变量指定的目录 (LIBRARY_PATH)
由系统指定的目录: /usr/lib /usr/local/lib
生成动态库
shared: 表示生成共享库格式
fPIC:产生位置无关码(position independent code)
库名规则:libxxx.so
使用动态库
编译选项:
l:链接动态库,只要库名即可(去掉lib以及版本号)
L:链接库所在的路径.
运行动态库
1、拷贝.so文件到系统共享库路径下, 一般指/usr/lib
2、更改 LD_LIBRARY_PATH
3、ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新
站在库的制作者角度
库中是没有main函数的,我们简易制作库时也不能把main函数打入库中
静态库:
Add.h
#pragma once
#include <stdio.h>
extern int Add(int,int);
Add.c
#include "Add.h"
int Add(int x,int y)
{
return x + y;
}
Sub.h
#pragma once
#include <stdio.h>
extern int Sub(int,int);
Sub.c
#include "Sub.h"
int Sub(int x,int y)
{
return x - y;
}
Mul.h
#pragma once
#include <stdio.h>
extern int Mul(int,int);
Mul.c
#include "Mul.h"
int Mul(int x,int y)
{
return x * y;
}
Div.h
#pragma once
#include <stdio.h>
extern int Div(int,int,int*);
Div.c
#include "Div.h"
int Div(int x,int y, int *code)
{
*code = 0;
if(y == 0)
{
*code = -1;
return -1;
}
return x / y;
}
Makefile
1 Test:Add.o Div.o Mul.o Sub.o TestMain.o
2 gcc -o $@ $^
3
4 %.o:%.c
5 gcc -c $<
6
7 .PHONY:clean
8 clean:
9 rm -f *.o Test
TestMain.c
1 #include "Add.h"
2 #include "Sub.h"
3 #include "Div.h"
4 #include "Mul.h"
5
6
7 int main()
8 {
9 int x = 10;
10 int y = 20;
11
12 printf("%d+%d=%d\n", x, y, Add(x, y));
13 printf("%d-%d=%d\n", x, y, Sub(x, y));
14 printf("%d*%d=%d\n", x, y, Mul(x, y));
15
16 return 0;
17 }
我们可以把Add.o Div.o Mul.o Sub.o打包,用户使用方法时将其与用户程序链接:
Makefile:
1 static-lib=libmymath.a //形成自己的静态库
2 $(static-lib):Add.o Div.o Mul.o Sub.o
3 ar -rc $@ $^
4
5 %.o:%.c
6 gcc -c $<
7
8 .PHONY:clean
9 clean:
10 rm -f *.o *.a
静态库本质就是将库中的源代码直接翻译成为.o目标二进制文件,然后打包
动态库:
Makefile:
dy-lib=libmymath.so
$(dy-lib):Add.o Div.o Mul.o Sub.o
gcc -shared -o $@ $^
%.o:%.c
gcc -fPIC -c $<
.PHONY:output
output:
mkdir -p mymath_lib/include
mkdir -p mymath_lib/lib
cp -f *.h mymath_lib/include
cp -f *.so mymath_lib/lib
.PHONY:clean
clean:
rm -rf *.o *.so mymath_lib
站在库的使用者角度
静态库:
我们写的静态库默认是第三方库,gcc不认识
目前介绍到这里,我们使用自己写的静态库,需要.h和.a
ldd 程序名 无法查找该程序所依赖的静态库,静态库也已经拷贝到可执行程序里了
gcc形成可执行程序时,加上-static 意味着必须静态链接,不能静态链接直接报错,不加-static 则能动态链接就动态链接,只能静态链接就静态链接
即:gcc默认动态链接,但个别库如果只提供.c,gcc也没办法,只能局部性地把你指定的.a进行静态链接,其他库正常动态链接,如果加上-static,就必须要.a
在上述代码中,我们使用自己写的静态库,需要提供.h和.a,不优雅,现在改良成最终版:
Makefile:
2 $(static-lib):Add.o Div.o Mul.o Sub.o
3 ar -rc $@ $^
4
5 %.o:%.c
6 gcc -c $<
7
8 .PHONY:output
9 output:
10 mkdir -p mymath_lib/include
11 mkdir -p mymath_lib/lib
12 cp -f *.h mymath_lib/include
13 cp -f *.a mymath_lib/lib
14
15
16 .PHONY:clean
17 clean:
18 rm -rf *.o *.a mymath_lib
我们可以上传打包的压缩包到网上以供下载,我们也可以下载网上的压缩包,作为库来链接
解压获得库并删去压缩包
动态库:
动态库的形成和使用基本上和静态库没差别,无非就是动态库形成时我们使用gcc+特定的选项
出人意料的是该程序运行不了:
因为程序加载运行起来后,主要是操作系统的part了,我们告诉的是编译器gcc自己的动态库在哪里,并没有告诉操作系统,(它有自己的默认搜索路径)
加载程序时,也需要把动态库找到并加载进来(静态库不用加载,它已经拷贝到程序中) 可是系统默认找不到
解决方法一:把需要的头文件和库文件直接安装到系统里
这样一来,我们形成可执行程序时就可以简化:
ldd a.out:
解决方法二:使用软链接,查找动态库
如果动态库在当前目录下 是可以直接被找到的,我们可以通过建立软链接的方式来找到对应的库
解决方法三:使用环境变量让系统找到自己的动态库
环境变量:LD_LIBRARY_PATH(加载库的意思)
系统还会去该环境变量所记录的路径中查找所需要链接的库
我们的程序在加载运行时,知道自己要链接哪个动态库,只是找不到,所以我们只需要获取该动态库的路径
将动态库所在路径导入到环境变量LD_LIBRARY_PATH里
但export导入环境变量只是在内存中修改,重新登录后就无了
解决方法四:直接更改系统关于动态库的配置文件
在该目录下新建一个自己的配置文件xx.conf
然后 sudo vim打开这个配置文件
将动态库所在路径粘贴
同一组库,提供动静两种库,gcc默认使用动态库
动态库加载
在linux下,可执行程序是ELF格式的,动态链接的程序,不仅它自己要加载,它所链接的库也要加载
程序没有被加载时,程序内部是有地址的,变量名函数名等编译成为了二进制就没有了,全部变成地址了,编译的时候对代码进行编址,基本遵循虚拟地址空间那一套,虚拟地址空间不仅仅是OS里面的概念,编译器编译的时候也要按照这样的规则编译可执行程序,这样才能在加载的时候,进行从磁盘文件到内存,再进行映射
形成库中函数的地址适合用相对编址,库可以加载到内存的任意位置,只要有偏移量,那么函数地址就可以找到,不用发生变化
库被加载之后要被映射到指定使用了该库们的进程的地址空间中的共享区部分