目录
前言:这是Linux基础IO的一大块内容,包括了Linux的系统IO接口的介绍及使用、理解文件描述符、重定向和缓冲区,标准输入、输出、错误,文件系统,软硬链接,动态库和静态库。
一.文件描述符
① 文件 = 文件内容 + 文件属性(文件属性也是数据,即使创建一个空文件,也要占据磁盘空间)
② 文件操作 = 文件内容的操作 + 文件属性的操作
③ “打开”文件就是将文件的属性或内容加载到内存中
④ 没有被打开的文件在磁盘上存储着
⑤ 打开的文件:内存文件;另一种文件是:磁盘文件
⑥ 当文件程序运行起来的时候,才会执行对应的代码,然后才是真正的对文件进行相关的操作
二.C语言文件操作
① r:只读(为了输入数据,打开一个已经存在的文本文件)
② r+:读写(为了读和写,打开一个文本文件)
③ w:只写(为了输出数据,打开一个文本文件)
④ w+:读写(为了读和写,建立一个新的文件)
⑤ a:追加(向文本文件尾添加数据)
⑥ a+:追加读写(打开一个文件,在文件尾进行读写)
当前路径是当前进程所处的工作路径。
追加写入,是不断的往文件中新增内容 ———— 也就是追加重定向。
当我们以w方式打开文件,准备写入的时候,文件已经先清空了。
在向文件写入时,最终就是向磁盘写入(磁盘是硬件)。而有资格向硬件写入的只有操作系统,因此在调用上层访问文件的操作(比如C语言的文件操作函数)都必须贯穿操作系统。
而上层访问文件的操作都是封装了系统接口,所有的语言都对系统接口做了封装。
为什么要封装呢?
① 原生的系统接口,使用成本比较高
② 直接使用系统接口的语言不具备跨平台性
因此语言通过封装(穷举所有的底层接口+条件编译)具有了跨平台性。
因此为了更好的理解操作系统,我们要学习原生的系统接口。
三.系统IO接口
#include <stdio.h>
#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()
{
printf("PRINT_DFL:\n");
Show(PRINT_DFL);
printf("PRINT_A\n");
Show(PRINT_A);
printf("PRINT_B\n");
Show(PRINT_B);
printf("PRINT_A 和 PRINT_B\n");
Show(PRINT_A | PRINT_B);
printf("PRINT_C 和 PRINT_D\n");
Show(PRINT_C | PRINT_D);
printf("PRINT all:\n");
Show(PRINT_A | PRINT_B | PRINT_C | PRINT_D);
return 0;
}
Linux中系统中的接口就类似于上面实现的这样,下面的O_WRONLY这种的宏定义都跟上面一样,采用位图的思想来实现。
1.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
这里的write在下一块介绍了。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
// fopen("log.txt", "w"); //底层的open,O_WRONLY | O_CREAT | O_TRUNC
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC);
int cnt = 0;
const char *str = "hello file\n";
while(cnt < 5)
{
write(fd, str, strlen(str));
cnt++;
}
close(fd);
return 0;
}
结果为
这里因为我们使用open时,要创建一个新文件,但是我们没有指明新文件的访问权限(mode),就会导致访问权限完全乱码。
因此一定要指明访问权限。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
// fopen("log.txt", "w"); //底层的open,O_WRONLY | O_CREAT | O_TRUNC
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int cnt = 0;
const char *str = "hello file\n";
while(cnt < 5)
{
write(fd, str, strlen(str));
cnt++;
}
close(fd);
return 0;
}
这时,就正常了,但是我们又会发现我们设置的访问权限是0666,而这里显示的却是0664,这是因为有umask的存在。
因此,我们为了防止出现这种情况,我们可以在最前面自己设置umask(在代码中,umask不只是指令,也是函数)。因为我们不确定当前系统的umask是否是0002,如果不是,那么还没有自己设置umask,那么就会不一定会发生什么bug。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0);
// fopen("log.txt", "w"); //底层的open,O_WRONLY | O_CREAT | O_TRUNC
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int cnt = 0;
const char *str = "hello file\n";
while(cnt < 5)
{
write(fd, str, strlen(str));
cnt++;
}
close(fd);
return 0;
}
这时就真正的完全与预期相同了。
这里也要注意是strlen(str),不能再-1了,linux的系统接口不是C语言,C语言识别'\0',系统接口并不识别,如果再-1就会出现错误,如下:
因此不能-1。
如果我们在原来log.txt的基础上,再次以写的方式write:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0);
// fopen("log.txt", "w"); //底层的open,O_WRONLY | O_CREAT | O_TRUNC
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int cnt = 0;
const char *str = "abcd";
while(cnt < 1)
{
write(fd, str, strlen(str));
cnt++;
}
close(fd);
return 0;
}
如果我们没有加O_TRUNC就会导致这样:
只有加上了O_TRUNC才会变成正确的:
如果我们想要以追加的方式open,就使用O_APPEND。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0);
// fopen("log.txt", "a"); //底层的open,O_WRONLY | O_CREAT | O_APPEND
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int cnt = 0;
const char *str = "bbbbbb";
while(cnt < 1)
{
write(fd, str, strlen(str));
cnt++;
}
close(fd);
return 0;
}
2.write
ssize_t write(int fd, const void *buf, size_t count);
write的参数第一个是文件描述符,第二个是要写的内容的那个指针,第三个是写入的个数
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
fopen("log.txt", "w"); //底层的open,O_WRONLY | O_CREAT | O_TRUNC
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int cnt = 0;
const char *str = "hello file\n";
while(cnt < 5)
{
write(fd, str, strlen(str));
cnt++;
}
close(fd);
return 0;
}
3.read
ssize_t read(int fd, void *buf, size_t count);
read的参数第一个是文件描述符,第二个是存储要读的内容的那个指针,第三个是读取的个数
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
fopen("log.txt", "w"); //底层的open,O_WRONLY | O_CREAT | O_TRUNC
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
char buffer[128];
ssize_t s = read(fd, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = '\0';
printf("%s", buffer);
}
close(fd);
return 0;
}
4.close
int close(int fd);
这个只有一个参数,就是文件描述符。
5.文件描述符
(1)理解文件描述符
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
umask(0);
int fda = open("loga.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fdb = open("logb.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fdc = open("logc.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fdd = open("logd.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fde = open("loge.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fda: %d\n", fda);
printf("fdb: %d\n", fdb);
printf("fdc: %d\n", fdc);
printf("fdd: %d\n", fdd);
printf("fde: %d\n", fde);
return 0;
}
这时的结果是
fda是从3开始的,这是为什么呢?
因为0,1,2已经默认打开了。
0:标准输入,键盘
1:标准输出,显示器
2:标准错误,显示器
C语言中提供了FIFE*(文件指针),FIFE是C语言库中提供的结构体,封装了多个成员(里面就包括了fd),上面的0,1,2就分别是stdin、stdout、stderr。
文件描述符从0开始,0,1,2,3,4,5......,其实就是数组下标,它的内部实现就是一个数组。
(2)内部实现
一个进程是可以打开多个文件的,因此进程是在内核中的。
系统在运行中,有可能会存在大量的被打开的文件,OS一定要对这些被打开的文件进行管理,而管理的方式就是先描述,在组织。
进程:打开的文件 = 1:n 那么进程如何和打开的文件建立映射关系呢?
首先这个file是一个链表的结构,因此对被打开的文件的管理,就转化成为了对链表的增删查改。
struct file
{
struct file *next;
struct file *prev;
}
其内部就与take_struct有关了
这个file指针是存在file_struct里的,而这个file_struct又是存在take_struct里的。在file_struct里,有很多个file的指针,这个指针是存在一个数组中的,因此是从0开始的。
但是呢,stdin是键盘,stdout是显示器,stderr是显示器,这些都是硬件,为什么能用struct file来标识对应的文件呢?
因为linux一切皆文件。
如何用C语言,实现面向对象呢?
使用函数指针。
使用函数指针可以让C语言实现面向对象,而这也与C++底层类的实现有关,而this指针其实就是与这个file*是同样的道理。
(3)文件描述符的分配规则
在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
close(0);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
结果是
因为把0给close了,所以就从头开始,新open的fd就是0。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
close(2);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
结果是
这个也是如此。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
close(1);
// 根据fd的分配规则,新的fd值一定是1
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
printf->stdout->1->虽然不在指向对应的显示器了,但是已经指向了log.txt的底层struct file对象!
printf("fd: %d\n", fd);
close(fd);
return 0;
}
结果是
没有fd结果,这是因为fd为1是在显示器输出,如果1被close了,那么就无法再输出到显示器了,那么就不会显示出结果。但是因为新open的文件占了1,那么它应该被输入到了文件中,这时我们查看改文件:
依旧是空的。
这就跟重定向和缓冲区有关了。如果我们在printf后面加上fflush就会有结果,至于原因,将会在之后说明。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
close(1);
// 根据fd的分配规则,新的fd值一定是1
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
printf->stdout->1->虽然不在指向对应的显示器了,但是已经指向了log.txt的底层struct file对象!
printf("fd: %d\n", fd);
// 为什么必须要fflush? 这个就和我们历史的缓冲区有关了
fflush(stdout);
close(fd);
return 0;
}
fflush之后就会有结果了。
(4)重定向
① 介绍
如果我们要进行重定向,因为上层只认0,1,2,3,4,5这样的fd,因此我们可以在OS内部,通过一定的方式调整数组的特定下标的内容(指向),就可以完成重定向操作。
② 具体操作
上面的一堆的数据,都是内核数据结构,只有OS有权限。 ->OS必定会提供重定向的接口。
int dup2(int oldfd, int newfd);
dup2就是linux系统中用来重定向的系统接口。
这里将newfd的内容拷贝到oldfd中,最后只剩oldfd。
如果想要输出重定向,将内容输出到文件中,就应该是dup2(fd, 1)。
输出重定向:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open");
return 0;
}
dup2(fd, 1);
//本来应该要往显示器打印,最终却变成了向指定文件打印 -> 重定向
fprintf(stdout, "打开文件成功,fd: %d\n", fd);
fflush(stdout);
close(fd);
return 0;
}
这样本来打印到显示器的,就被重定向到了文件标识符为fd的那个文件中。
追加重定向:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
if(fd < 0)
{
perror("open");
return 0;
}
dup2(fd, 1);
//本来应该要往显示器打印,最终却变成了向指定文件打印 -> 重定向
fprintf(stdout, "打开文件成功,fd: %d\n", fd);
fflush(stdout);
close(fd);
return 0;
}
这里只要将O_TRUNC改成O_APPEND就可以将输出重定向变为追加重定向。
输入重定向:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt", O_RDONLY);
if(fd < 0)
{
perror("open");
return 0;
}
dup2(fd, 0);
char line[64];
while(fgets(line, sizeof line, stdin) != NULL)
{
printf("%s", line);
}
fflush(stdout);
close(fd);
return 0;
}
要想变成输入重定向,只需要将dup2中的1变成0,即可从文件中读取内容。
(5)缓冲区
① 缓冲区本质
缓冲区就是一段内存
② 缓冲区的作用
a.解放使用缓冲区的进程时间
b.缓冲区的存在可以集中处理数据刷新,减少IO的次数,从而达到提高整机效率的目的
③ 缓冲区的位置
缓冲区是语言级别的 ,是在FILE内部的,在C语言中,我们每一次打开一个文件,都要有一个FILE*会返回。就意味着,每一个文件都要有一个fd和属于它自己的语言级别的缓冲区。
如果在刷新之前,就关闭了fd会导致无法显示出结果(因为fd被关闭了,之前存储在缓冲区的内容无法再通过write输出了)。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
// printf没有立即刷新的原因,是因为有缓冲区的存在
printf(" hello printf"); // stdout -> 1, -> 封装了write
fprintf(stdout, " hello fprintf");
fputs(" hello fputs", stdout);
// write会立即刷新
// 那么这个缓冲区一定不在write内部
// 说明缓冲区不是内核级别的
// 缓冲区只能是由语言提供的,是语言级别的
const char *msg = "hello write";
write(1, msg, strlen(msg));
sleep(5);
}
这里printf、fprintf和fputs都没有被打印出来,只有write打印了出来。更说明了缓冲区是语言级别的。
有这样一个问题:
int main()
{
const char *str1 = "hello printf\n";
const char *str2 = "hello fprintf\n";
const char *str3 = "hello fputs\n";
const char *str4 = "hello write\n";
//C库函数
printf(str1); //?
fprintf(stdout, str2);
fputs(str3, stdout);
//系统接口
write(1, str4, strlen(str4));
//是调用完了上面的代码,才执行的fork
fork();
}
看到这样的两个结果,我们可以会很疑惑,那么到底是为什么呢?
刷新的本质,是把缓冲区的数据write到OS内部,并且清空缓冲区。
缓冲区是在自己的FILE内部维护的,属于父进程内部的数据区域。
因此,进行fork时,不论谁先进行刷新清空,代码父子进程共享,数据父子进程各自以写时拷贝的方式形成一份,因此父进程刷一份,子进程刷一份,就会出现上图的情况。
(6)模拟实现封装的C标准库
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#define NUM 1024
#define NONE_FLUSH 0x0
#define LINE_FLUSH 0x1
#define FILL_FLUSH 0x2
typedef struct _MyFILE
{
int _fileno;
char _buffer[NUM];
int _end;
int _flags;
}MyFILE;
MyFILE *my_fopen(const char *filename, const char *method)
{
assert(filename);
assert(method);
int flags = O_RDONLY;
if(strcmp(method, "r") == 0)
{}
else if(strcmp(method, "r+") == 0)
{}
else if(strcmp(method, "w") == 0)
{
flags = O_WRONLY | O_CREAT | O_TRUNC;
}
else if(strcmp(method, "w+") == 0)
{}
else if (strcmp(method, "a") == 0)
{
flags = O_WRONLY | O_CREAT | O_APPEND;
}
else if (strcmp(method, "a+") == 0)
{}
int fileno = open(filename, flags, 0666);
if (fileno < 0)
{
return NULL;
}
MyFILE* fp = (MyFILE*)malloc(sizeof(MyFILE));
if (fp == NULL)
return fp;
memset(fp, 0, sizeof(MyFILE));
fp->_fileno = fileno;
fp->_flags |= LINE_FLUSH;
fp->_end = 0;
return fp;
}
void my_fflush(MyFILE* fp)
{
assert(fp);
if (fp->_end > 0)
{
write(fp->_fileno, fp->_buffer, fp->_end);
fp->_end = 0;
syncfs(fp->_fileno);
}
}
void my_fwrite(MyFILE* fp, const char* start, int len)
{
assert(fp);
assert(start);
assert(len > 0);
// 写到缓冲区里面
strncpy(fp->_buffer + fp->_end, start, len);
fp->_end += len;
if (fp->_flags & NONE_FLUSH)
{
}
else if (fp->_flags & LINE_FLUSH)
{
if (fp->_end > 0 && fp->_buffer[fp->_end - 1] == '\n')
{
write(fp->_fileno, fp->_buffer, fp->_end);
fp->_end = 0;
syncfs(fp->_fileno);
}
}
else if (fp->_flags & FILL_FLUSH)
{
}
}
void my_fclose(MyFILE * fp)
{
my_fflush(fp);
close(fp->_fileno);
free(fp);
}
int main()
{
MyFILE* fp = my_fopen("log.txt", "w");
if (fp == NULL)
{
printf("my_fopen error\n");
return 1;
}
const char *s = "hello my 111\n";
my_fwrite(fp, s, strlen(s));
printf("消息立即刷新\n");
sleep(3);
const char *ss = "hello my 222";
my_fwrite(fp, ss, strlen(ss));
printf("写入了一个不满足刷新条件的字符串\n");
sleep(3);
const char *sss = "hello my 333";
my_fwrite(fp, sss, strlen(sss));
printf("写入了一个不满足刷新条件的字符串\n");
sleep(3);
const char *ssss = "end\n";
my_fwrite(fp, ssss, strlen(ssss));
printf("写入了一个满足刷新条件的字符串\n");
sleep(3);
/*const char* s = "-aaaaaaa";
my_fwrite(fp, s, strlen(s));
printf("写入了一个不满足刷新条件的字符串\n");
fork();*/
// 模拟进程退出
my_fclose(fp);
return 0;
}
通过以上结果,我们可以很清楚的看出来,加了\n的会立即进行刷新,而没有加\n的会在进程准备退出的时候,刷新缓冲区。
int main()
{
MyFILE* fp = my_fopen("log.txt", "w");
if (fp == NULL)
{
printf("my_fopen error\n");
return 1;
}
const char *s = "hello my 111\n";
my_fwrite(fp, s, strlen(s))
const char* s = "-aaaaaaa";
my_fwrite(fp, s, strlen(s));
printf("写入了一个不满足刷新条件的字符串\n");
fork();
// 模拟进程退出
my_fclose(fp);
return 0;
}
如果将main改成如上这样,结果是:
这个就是因为数据放在了缓冲区中,然后父子进程同时刷新,出现两条。
(7)模拟shell实现增加重定向功能
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <ctype.h>
#define SEP " "
#define NUM 1024
#define SIZE 128
#define DROP_SPACE(s) do { while(isspace(*s)) s++; }while(0)
char command_line[NUM];
char* command_args[SIZE];
char env_buffer[NUM];
#define NONE_REDIR -1
#define INPUT_REDIR 0
#define OUTPUT_REDIR 1
#define APPEND_REDIR 2
int g_redir_flag = NONE_REDIR;
char* g_redir_filename = NULL;
extern char** environ;
//对应上层的内建命令
int ChangeDir(const char* new_path)
{
chdir(new_path);
return 0; // 调用成功
}
void PutEnvInMyShell(char* new_env)
{
putenv(new_env);
}
void CheckDir(char* commands)
{
assert(commands);
char* start = commands;
char* end = commands + strlen(commands);
// ls -a -l
while (start < end)
{
if (*start == '>')
{
if (*(start + 1) == '>')
{
// ls -a -l >> log.txt ---- 追加重定向
*start = '\0';
start += 2;
g_redir_flag = APPEND_REDIR;
DROP_SPACE(start);
g_redir_filename = start;
break;
}
else
{
// ls -a -l > log.txt ---- 输出重定向
*start = '\0';
start++;
DROP_SPACE(start);
g_redir_flag = OUTPUT_REDIR;
g_redir_filename = start;
break;
}
}
else if (*start == '<')
{
// 输入重定向
*start = '\0';
start++;
DROP_SPACE(start);
g_redir_flag = INPUT_REDIR;
g_redir_filename = start;
break;
}
else
{
start++;
}
}
}
int main()
{
// shell 本质上就是一个死循环
while (1)
{
g_redir_flag = NONE_REDIR;
g_redir_filename = NULL;
// 1.显示提示符
printf("[李四@我的主机名 目录]# ");
fflush(stdout);
// 2.获取用户输入
memset(command_line, '\0', sizeof(command_line) * sizeof(char));
fgets(command_line, NUM, stdin); // 键盘,标准输入,stdin,获取到的是c风格的字符串,'\0'
command_line[strlen(command_line) - 1] = '\0'; // 清空\n
// ls -a -l > log.txt or cat < log.txt or ls -a -l >> log.txt or ls -a -l
// ls -a -l > log.txt -> ls -a -l\0log.txt
CheckDir(command_line);
// 3.字符串划分"ls -a -l -i" -> "ls" "-a" "-l" "-i"
command_args[0] = strtok(command_line, SEP);
int index = 1;
if (strcmp(command_args[0]/*程序名*/, "ls") == 0)
{
command_args[index++] = (char*)"--color=auto";
}
// strtok截取成功,返回字符串起始地址
// 截取失败,返回NULL
while (command_args[index++] = strtok(NULL, SEP));
// 4. 内建命令
if (strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
{
ChangeDir(command_args[1]); //让调用方进行路径切换, 父进程
continue;
}
if (strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
{
// 目前,环境变量信息在command_line,会被清空
// 此处我们需要自己保存一下环境变量内容
strcpy(env_buffer, command_args[1]);
PutEnvInMyShell(env_buffer); //export myval=100, BUG?
continue;
}
// 5.创建进程,执行
pid_t id = fork();
if (id == 0)
{
int fd = -1;
switch (g_redir_flag)
{
case NONE_REDIR:
break;
case INPUT_REDIR:
fd = open(g_redir_filename, O_RDONLY);
dup2(fd, 0);
break;
case OUTPUT_REDIR:
fd = open(g_redir_filename, O_WRONLY | O_CREAT | O_TRUNC);
dup2(fd, 1);
break;
case APPEND_REDIR:
fd = open(g_redir_filename, O_WRONLY | O_CREAT | O_APPEND);
dup2(fd, 1);
break;
default:
printf("Bug?\n");
break;
}
// child
// 6.进程替换 不会影响曾经子进程打开的文件
execvp(command_args[0], command_args);
exit(1); // 执行到这里,子进程一定替换失败
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0)
{
printf("等待子进程成功:sig:%d, code:%d\n", status & 0x7F, (status >> 8) & 0xFF);
}
}
}
6.标准输出、标准错误
(1)区别
#include <iostream>
#include <cstdio>
int main()
{
stdout
printf("hello printf 1\n");
fprintf(stdout, "hello fprintf 1\n");
fputs("hello fputs 1\n", stdout);
//stderr
fprintf(stderr, "hello fprintf 2\n");
fputs("hello fputs 2\n", stderr);
perror("hello perror 2");
//cout
std::cout << "hello cout 1" << std::endl;
//cerr
std::cerr << "hello cerr 2" << std::endl;
}
正常运行是全部输出的。
如果我们将其重定向到stdout.txt中,会发现带1的(标准输出)都没了,都进入到了stdout.txt中,而带2的(标准错误)进不去。
如果写成这样,则可以让带1的进入stdout.txt中,让带2的进入stderr.txt中
标准写法是下面这样:
如果想要全部重定向到一个文件中:
all中就有了全部的输出。
(2)意义
通过上面的实验,我们可以发现标准输出与标准错误是有区别的,那么到底有什么意义呢?
可以区分哪些是程序日常输出,哪些是错误。
这个在实际中可以由日志等级来区分。
在平时写的过程中,标准输出和标准错误都用对应的最好。
(3)errno
如果我们翻阅man手册,会看到很多errno这个词,这个erron在C语言就是一个全局变量,用来记录最近一次C库函数调用失败的原因。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_RDONLY); //必定失败的
if(fd < 0)
{
perror("open");
return 1;
}
这里open:后面的就是错误的信息。
模拟实现:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
void my_perror(const char *info)
{
fprintf(stderr, "%s: %s\n", info, strerror(errno));
}
int main()
{
int fd = open("log.txt", O_RDONLY); //必定失败的
if(fd < 0)
{
my_perror("my open");
return 1;
}
return 0;
}
可以看出与系统的输出一样。
四.文件系统
上面的系统IO接口都是在内存中的,但是并不是所有的文件都被打开了。
大量的文件,都是在磁盘上待着,等待被调用,并且这批文件非常的多且杂。
而磁盘基本的文件管理,本质就是为了能够对磁盘这块大空间进行合理划分,让我们能够快速定位、查找到指定的文件,进行后续的访问操作。这个模块就叫做文件系统。
1.磁盘的物理结构
磁盘是电脑上唯一的一个机械设备(文件数据就在这个盘面上)
磁盘有很多个盘面,一个盘面有一个磁头。
磁盘是具有磁性的,分为N、S级,改变NS级,就是改变了01。
因为磁盘是机械式的外设,所以磁盘很慢(和CPU、内存比)。
2.磁盘的存储结构
磁盘上存储的基本单元是扇区512字节。
读写磁盘的时候,磁盘找的是某一个面(哪一个磁头)的某一个磁道(哪一个柱面【距离圆心的半径】)的某一个扇区(磁道上的一段【盘面旋转决定的】)。
磁柱:Cylinder;磁面:Head;扇区:Sector
只要我们能找到磁盘上的(CHS地址)盘面,柱面(磁道),扇区,就能找到一个存储单元。
用同样的方法,就可以找到所有的基本单元。
3.磁盘的逻辑抽象结构
磁带原本是缠在上面的圆形的,里面存的是数据,但是我们是可以把它拉出来,然后扯成直线的。
而磁盘的盘片其实也是如此,把盘片想象成为线性的结构。就可以把盘片当中是数组,定位有关sector,只要找到下标(LBA【逻辑块地址】)就可以了。
因此对磁盘的管理,就转化成为了对数组空间的管理。
因此内存里的数据想要往磁盘里写入 ,在内存中只知道有关地址叫LBA,然后将LBA地址映射转化为CHS地址,再将内存中的数据配合CHS写到磁盘里,完成磁盘的写入。
如果磁盘一共有4个面,一个面能存1000个数据,每个面有20个磁道知道LBA地址是1234,那么写入的过程:
3234/1000 = 3 -- 在第3面
3234%1000 = 234
234/20 = 11 -- 在第11个磁道
234%20 = 14 -- 在第14个扇区
C: 11
H: 3
S: 14
每一个扇区的大小是512字节,每一次访问只有512字节很小,效率差,因此操作系统会对其进行再一次抽象,以8个扇区为单位,整合为一个OS所认为的存储单元,所以大小就变成了4KB。OS在读写数据的时候,就会去这个存储单元中找。(IO的基本单位是4KB)
这样做的两个作用是:①提高IO效率 ② 不让软件(OS)和硬件(磁盘)具有强相关性,也就是解耦合。
又因为磁盘太大了,OS就会将其划分多个小区域,但是依旧很多,就再划分成多个小组,把每一个小部分都管理,分而治之,就管好了着整个大空间。这就是分区的过程。
只要管好局部,就能管好全部。
4.inode
文件 = 内容 + 属性 内容和属性都是数据都需要存储的。
内容是存在block中的,属性是存在inode中的(128字节),文件的属性是稳定的。
文件系统如下图:
① Book Block:与开机有关,里面包括了各种开机信息,有分区表,以及软件的位置信息。
② Block group:一整个是文件系统所划分的不同的组,每个组的结构构成都相同。
③ Date blocks:以块为单位,进行文件内容的保存(所占的空间最大,80%左右)。
④ inode Table:以128字节为单位,保存文件的属性,进行inode属性的保存。inode里面有一个inode编号。一般而言,一个文件,一个inode,一个inode编号。
前面的1970844和1970845就是文件的indo编号。
inode中有struct inode,里面包括了文件所有的属性blocks[15],
其中的[0, 11]:直接保存的就是该文件对应的blocks编号。
[12, 15] :指向一个datablock,但是整个datablock不保存有效数据,而保存该文件所适用的其它块的编号。(相当于一个二级索引)
⑤ Block Bitmap:这里按位记录着Date Block(数据块)哪个被占用,哪个没被占用。每个bit位为0表示没被占用,为1表示被占用
⑥ inode Bitmap:这里按位记录着inode的使用情况。每个bit位为0表示没被使用,为1表示被使用
⑦ Group Descriptor Table(GDT):对块组进行描述,包含了有多少inode,起始的inode编号,有多少个inode被使用,有多少block被使用,还剩多少等等信息。
⑧ Super Block: 就是我们文件系统的顶层数据结构,包含了整个分区有多少块,每个块组的使用情况,整个分区的文件系统是怎样的等等信息。
那么既然Super Block可以显示整个分区的信息,那么为什么会在组里,而不是组外面呢?
实际上,并不是每个组都有Super Block的,只是在一部分组中,有几个有,并且这些Super Block是完全相同的,它的主要目的是为了备份,如果放在外面,那么这个位置一旦出错,那么这整个区的信息就全部丢失了,而有了很多份备份,丢失了一份,还可以去从另一份中得到信息。
文件名,算文件的属性,但是inode里面,并不保存文件名。
因为Linux,底层实际都是通过inode编号标识文件的。
因此,要找到文件,必须找到文件的inode编号。
那是谁帮我们找到inode编号呢?
目录是一个文件,那就一定有属性和内容,也就是有biocks和inode。
那么它的blocks中放的是什么呢?
放的就是文件名和inode编号的映射关系。因此Linux同一个目录下,不可以创建多个同名文件。
文件名本身就是一个具有Key值的东西。
当我们创建一个文件,操作系统做了什么呢?
创建一个文件时,操作系统会修改inode Bitmap,由0置1,在inode Table中找到对应的inode节点,向里面写入对应的属性,并且为它分配数据块,把数据写到数据块中,同时修改block Bitmap,并且建立inode和block的映射关系,最终返回该文件的inode。
创建一个文件的时候,一定是在一个目录下。
文件名 inode编号 -> 找到自己所处的目录 -> 根据目录的inode,找到目录的data block -> 将文件名和inode编号的映射关系写入到目录的数据块中
删除一个文件,操作系统做了什么呢?
找到自己的目录的inode,再找到自己目录的blocks,然后根据文件名的唯一性,以及它与inode的映射关系,找到对应的inode编号,此时再根据inode编号找到它对应的Block group,然后将该文件所对应的inode Bitmap和block Bitmapp由1置0,就完成了文件的删除。
因此,Linux并没有真正的清除数据,只是将inode Bitmap和block Bitmapp由1置0,就相当于删除了。
所以,想要恢复整个删除的数据也很容易,只要知道了这个inode,通过一些工具,把这个inode对应的inode Bitmap和block Bitmapp由0恢复成1。
五.软硬链接
1.软硬链接的区别
软链接是一个独立文件,有自己独立的inode和inode编号。硬链接不是一个独立文件,它和它的目标文件使用的是同一个inode。
2.软链接
软链接就像是一个快捷方式。
上图中my.exe是软链接,就相当于是mytest.exe的快捷方式。
软链接既然是一个独立文件,inode是独立的,那么软链接的文件内容是什么呢?
软链接保存的内容就是指向的文件的所在路径。
3.硬链接
硬链接就是单纯的在Linux指定的目录下,给指定的文件新增 文件名和inode编号的映射关系。
这里我们可以看到,有一个数字发生了变化,在最开始是1,但是进行了一次硬链接后变成了2,所以可以说明这个数字就是指定的inode文件的硬链接数。
那么什么是硬链接数呢?
硬链接数本质就是该文件inode属性中的计数器count,标识有几个文件名和我的inode建立了映射关系。简言之,就是有几个文件名指向我的inode(文件本身)。
硬链接有什么用呢?
先看这个图,为什么文件默认硬链接数是1,目录默认硬链接数是2呢?
如果硬链接数是0,那么就应该是被关闭的文件了。所以至少应该从1开始。
普通文件的文件名,本身就和自己的inode具有映射关系,且只要一个,所以是1。
目录创建了之后,会在目录中自动创建两个文件 . 和 .. ,仔细看这个inode编号,会发现 . 和mydir的inode编号是一样的,所以目录的默认硬链接数是2。再仔细看,又会发现..和10_26_23这个目录的inode编号是一样的,而且观察pwd,会发现10_26_23是这个目录的上级路径。而 . 和 .. 是什么?不就是当前路径和上级路径嘛。
所以,我们也可以根据系统的硬链接数,不进入文件,估算出文件的目录数。
因此,硬链接的一个作用就是进行路径间切换。
4.删除方式
建议使用unlink来删除软硬链接的文件 。(unlink也可以删除普通文件,与rm没什么区别)
六.动态库和静态库
1.介绍动静态库
静态链接是将需要的代码拷贝到程序当中
动态链接是要调用的函数的地址和调用的地方关联起来,不需要拷贝。
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接。
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许
物理内存中的一份动态库会被用到该库的所有进程共用,节省了内存和磁盘空间。
2.生成动静态库(设计)
(1)静态库
看上面这个图,会发现,如果我把我的所有的.o给别人,别人也是可以连接使用,即使它原本不属于我。
这样它拷贝进去生成的就相当于一个静态库。
但是这样很麻烦,因此我们可以将其打包
打包命令 ar(归档文件)。
ar -rc libmymath.a mymath.o myprint.o
c是creat的意思,表示这个文件不存在要creat
Makefile实现如下:
limymath.a:mymath.o myprint.o
ar -rc libmymath.a mymath.o myprint.o
mymath.o : mymath.c
gcc -c mymath.c -o mymath.o -std=c99
myprint.o : myprint.c
gcc -c myprint.c -o myprint.o -std=c99
.PHONY : clean
clean :
rm - f *.o *.a
这样我们就完成了打包,实现了静态库libmymath.a
那么静态库怎么进行交互呢?
Makefile实现如下:
libmymath.a:mymath.o myprint.o
ar -rc libmymath.a mymath.o myprint.o
mymath.o : mymath.c
gcc -c mymath.c -o mymath.o -std=c99
myprint.o : myprint.c
gcc -c myprint.c -o myprint.o -std=c99
.PHNOY:static
static:
mkdir -p lib-static/lib
mkdir -p lib-static/include
cp *.a lib-static/lib
cp *.h lib-static/include
.PHONY:clean
clean:
rm -rf *.o *.a lib-static
这样就可以实现一个静态库并且给别人用。
(2)动态库
shared:表示生成共享库格式
fPIC:产生位置无关码(与位置无关)
libmymath.so:mymath.o myprint.o
gcc -shared -o libmymath.so mymath.o myprint.o -std=c99
mymath.o:mymath.c
gcc -fPIC -c mymath.c -o mymath.o -std=c99
myprint.o:myprint.c
gcc -fPIC -c myprint.c -o myprint.o -std=c99
PHONY:clean
clean:
rm -f *.o *.so
这样就形成了动态库。
那动态库怎么进行交互呢?
libmymath.so:mymath.o myprint.o
gcc -shared -o libmymath.so mymath.o myprint.o -std=c99
mymath.o:mymath.c
gcc -fPIC -c mymath.c -o mymath.o -std=c99
myprint.o:myprint.c
gcc -fPIC -c myprint.c -o myprint.o -std=c99
.PHONY:dyl
dyl:
mkdir -p lib-dyl/lib
mkdir -p lib-dyl/include
cp *.so lib-dyl/lib
cp *.h lib-dyl/include
.PHONY:clean
clean:
rm -f *.o *.so lib-dyl
(3)动静态库同时生成
Makefile实现如下:
.PHONY:all
all: libmymath.so libmymath.a
libmymath.so:mymath.o myprint.o
gcc -shared -o libmymath.so mymath.o myprint.o -std=c99
mymath.o:mymath.c
gcc -fPIC -c mymath.c -o mymath.o -std=c99
myprint.o:myprint.c
gcc -fPIC -c myprint.c -o myprint.o -std=c99
libmymath.a:mymath_s.o myprint_s.o
ar -rc libmymath.a mymath_s.o myprint_s.o
mymath_s.o:mymath.c
gcc -c mymath.c -o mymath_s.o -std=c99
myprint_s.o:myprint.c
gcc -c myprint.c -o myprint_s.o -std=c99
.PHONY:lib
lib:
mkdir -p lib-static/lib
mkdir -p lib-static/include
cp *.a lib-static/lib
cp *.h lib-static/include
mkdir -p lib-dyl/lib
mkdir -p lib-dyl/include
cp *.so lib-dyl/lib
cp *.h lib-dyl/include
.PHONY:clean
clean:
rm -rf *.o *.a *.so lib-static lib-dyl
这样就同时生成了lib-dyl的动态库和lib-static的静态库。
3.使用动静态库
(1)静态库
#include "mymath.h"
#include "myprint.h"
int main()
{
int start = 0;
int end = 100;
int result = addToVal(start, end);
printf("result: %d\n", result);
Print("hello world");
}
如果我们想要通过调用自己的静态库lib-static中的头文件,完成进程的运行,该怎么办呢?
如果我们直接去运行,就会报错。
因为" "中的头文件会在当前路径去查找,而 这两个头文件都在lib-static中,并不在当前路径,因此一定会报错
①将其拷贝到系统路径
系统当中的头文件一般在/usr/include/中,系统中的库一般在lib64。
所以我们可以将自己的头文件和库文件,拷贝到系统路径下。
现在报错就与之前不同了,现在是链接错误。
因为,这是第三方库(在之前,我们用的都是c/c++的库,gcc/g++默认就认识 c/c++的库),而链接第三方库必须要指定该第三方库的名称。
gcc -l(小写L) + 指明要链接的第三方库的名称
库的名字要去掉前缀,去掉后缀,剩下的才是名字。
这样,就可以调用了。
注意:不推荐使用这种方法,因为这种方法会污染系统的头文件和库。
测试完之后,一定要将该头文件和库删除(卸载)。
②指定搜索路径
gcc -I(大写i) + 链接的库名
gcc -L + 库搜索路径
这里就手动的指定了头文件的搜索路径,就变成了链接错误。
因此我们要加上-L,去手动指定库文件搜索路径。
但是,我们会发现依旧是错的。
因为我们仅仅指定了库的路径,而没有指定是这个库路径中的哪个库。
因此又需要用到-l,去指定链接的库。
这时,才是真正的使用了这个静态库。
(2)动态库
①将其拷贝到系统路径
与静态库相同。
②指定搜索路径
依旧与静态库相同。
但是如果运行该进程,就会出现如下错误
通过ldd查看该文件的库:
因为-I,-L,-l都是告诉编译器gcc信息的,然后形成了可执行程序,形成了之后,就与gcc没有关系了,这时运行该可执行程序时,就不知道该进程的库在哪里了,所以无法运行。
静态库,在形成了可执行程序之后,已经把需要的代码拷贝进了我的代码中。这样运行时,就不需要依赖别的库了。(不需要运行时查找)
而动态库会找不到进程的库在哪,这是为什么呢?
程序和动态库是分开加载的。
动态库在运行期间,可以被多个进程所共享,所以也叫做共享库。
动态库在加载的时候,需要先找到在哪里,就要先告诉操作系统路径在哪里,而操作系统只会去默认路径找,但是找不到,就需要我们告诉操作系统动态库在哪里。
怎么解决呢?
让进程找到动态库!
方法①
将动态库拷贝到系统路径下/lib64
方法②
通过导入环境变量的方式。
程序运行的时候,会在环境变量中查找自己需要的动态库路径 ---- LD_LIBRARY_PAT
这时,再ldd,就会发现libmymath.so-> /home/hb/code/10_27_24/test/lib-dyl/lib/
然后,再运行这个可执行程序,就可以正常运行了。
注意:如果关掉了xshell,再次进来时,环境变量就会消失,就需要重新导入了。
方法③
系统配置文件。
这个路径表明的是系统里面,如果自定义了动态库,系统在扫描系统的路径时,除了在系统对应的库路径扫描对应的库之外,还会一个一个的去读取里面的配置文件,然后根据配置文件里的内容去找到对应的动态库。
这个配置文件里面其实就是一个路径
我们可以在里面创建一个文件。
vim打开它,对其进行配置,找到对应路径的动态库。
成功将其插入了进去。
这时ldd之后,会发现libmymath.so依旧是not fount,怎么办呢?
ldconfig /etc/ld.so.conf.d/myfile.conf
ldconfig就是让这个配置文件生效,也就是会将这个配置文件加载到内存空间里,让系统可以找到它。
加载完成之后,就会在libmymath.so中看到 /home/hb/code/10_27_24/test/lib-dyl/lib/
这时,就可以正常运行该可执行程序了。
并且关闭了xshell之后,再重新打开,也依旧不会消失。
不用了,就将其删掉。
方法④
在系统中建立软链接。
首先是建立软链接。
建立完之后,就会在libmymath.so中看到 /home/hb/code/10_27_24/test/lib-dyl/lib/
这时,就可以正常运行该可执行程序了。
建立了软链接,在编译代码的时候,只需要 -I(大写i)和 -l(小写L)即可找到对应的库。