#include
#include
#include
#include
#include
#include
#include
#include
#define NUM 1024
#define NONE_FLUSH 0x0 //无缓冲
#define LINE_FLUSH 0x1 //行缓冲
#define FULL_FLUSH 0x2 //全缓冲
typedef struct My_FILE
{
int _fileno;
char _buffer[NUM];
int _end;
int _flags;//flush method
}My_FILE;
My_FILE* 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;
}
My_FILE* fp = (My_FILE*)malloc(sizeof(My_FILE));
if(fp == NULL)
{
return fp;
}
memset(fp, 0, sizeof(My_FILE));
fp->_fileno = fileno;
fp->_flags |= LINE_FLUSH;
fp->_end = 0;
return fp;
}
void my_fwrite(My_FILE* 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 & FULL_FLUSH)
{}
}
void my_fflush(My_FILE* fp)
{
assert(fp);
if(fp->_end > 0)
{
write(fp->_fileno, fp->_buffer, fp->_end);
fp->_end = 0;
syncfs(fp->_fileno);//将数据写到磁盘中
}
}
void my_fclose(My_FILE* fp)
{
my_fflush(fp);
close(fp->_fileno);
free(fp);
}
int main()
{
My_FILE* fp = my_fopen("log.txt", "w");
if(fp == NULL)
{
printf("my_fopen error\n");
return 1;
}
const char* s = "hello file";
my_fwrite(fp, s, strlen(s));
printf("写入了不满足刷新条件的字符串\n");
fork();
my_fclose(fp);
return 0;
}
运行截图:
代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define NUM 1024
#define SIZE 128
#define SEP " "
#define ENV_MAXNUM 20//限定可以导入环境变量数目的最大值
#define NONE_REDIR -1
#define INPUT_REDIR 0
#define OUTPUT_REDIR 1
#define APPEND_REDIR 2
#define DROP_SPACE(start) do{while(isspace(*start)) start++;}while(0);
int g_redir_flag = -1;
char*g_redir_filename = NULL;
char env_buffer[ENV_MAXNUM][NUM];//存储环境变量
char command_line[NUM];
char* command_args[SIZE];
extern char** environ;
int Change_args(char* newPath)
{
chdir(newPath);
return 0;
}
void PutEnv(char* newEnv)
{
putenv(newEnv);
}
void CheckDir(char* commands)
{
assert(commands);
char* start = commands;
char* end = commands + strlen(commands);//end解引用就是'\0'
while(start < end)
{
if(*start == '>')
{
if(*(start+1) == '>')//说明此时是追加重定向
{
*start = '\0';
start++;
*start = '\0';
g_redir_flag = APPEND_REDIR;
start++;
DROP_SPACE(start);
g_redir_filename = start;
}
else//说明此时是输出重定向
{
*start = '\0';
g_redir_flag = OUTPUT_REDIR;
start++;
DROP_SPACE(start);
g_redir_filename = start;
}
break;
}
else if(*start == '<')//此时是输入重定向
{
*start = '\0';
g_redir_flag = INPUT_REDIR;
start++;
DROP_SPACE(start);
g_redir_filename = start;
break;
}
else
{
start++;
}
}
}
int main()
{
static int count = 0;//记录环境变量的起始下标
//shell本质上就是一个死循环
while(1)
{
g_redir_flag = -1;
g_redir_filename = NULL;
//不关心获取这些属性的接口
//1.显示提示符
printf("[zs@VM-0-3-centos 当前目录]# ");
fflush(stdout);
//2.获取用户输入
memset(command_line,'\0',sizeof(command_line)*sizeof(char));
fgets(command_line, NUM, stdin);//从标准输入中获取的,获取到的是C风格的字符串
command_line[strlen(command_line)-1] = '\0';
//处理重定向相关操作
//ls -a -l>log.txt / cat>log.txt
CheckDir(command_line);
//3.分割字符串 "ls -a -l" "ls" "-a" "-l"
command_args[0] = strtok(command_line, SEP);
int index = 1;
//4.TODO,处理内建命令
if(strcmp(command_args[0], "ls") == 0)
command_args[index++] = (char*)"--color=auto";
while(command_args[index++] = strtok((char*)NULL, SEP));
//处理cd内建命令
if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
{
Change_args(command_args[1]);
continue;
}
//处理内建命令export
if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
{
//目前环境变量信息在command_line中,会被清空
//此处我们需要自己保存一下环境变量的内容
strcpy(env_buffer[count], command_args[1]);
PutEnv(env_buffer[count++]);//将command_args[1]导入到环境变量中
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, 0666);
dup2(fd,1);
break;
case APPEND_REDIR:
fd = open(g_redir_filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd,1);
break;
default:
printf("Bug!\n");
break;
}
//child
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);
}
}//end while
return 0;
}
注意:C++中后缀不只可以是.cpp还可以是.cc、.cxx。
代码:
#include
#include
int main()
{
//stdout
printf("hello printf 1\n");
fprintf(stdout, "hello fprintf 1\n");
fputs("hello fputs 1\n", stdout);
//stderr
perror("hello perror 2\n");
fprintf(stderr, "hello fprintf 2\n");
fputs("hello fputs 2\n", stderr);
//cout
std::cout << "hello cout 1" << std::endl;
//cerr
std::cerr << "hello cerr 2" << std::endl;
return 0;
}
运行截图:
我们看到当我们进行重定向输出到log,.txt文件的时候只是将stdout中的内容重定向到了log.txt文件中,那么我们该如何将stderr中的内容也重定向到文件中呢?
注意:stdout和stderr虽然都是通过显示器进行打印,但他们始终都是不同的:
那么上面操作的意义是什么呢?
可以区哪些是日常输出,哪些是错误,进而更好的查看错误信息和定位错误地址。
问:如果我们想要将两者都混合起来输出到一个文件中,该如何操作呢?
问:上面的:Success是什么?
答:C语言中有一个全局变量errno,记录最近一次库函数调用失败的原因。
管理磁盘中文件的系统叫做文件系统。
磁盘是我们电脑上唯一的一个机械设备。当然,我们电脑上目前用的更多的是机械硬盘和固态硬盘。
问:磁盘是如何存储数据的?
答:磁盘是有磁性的,一般是通过N/S即来进行区分0/1的。
注意:磁盘访问的基本单位是扇区,但并不是说明访问磁盘必须按照一次访问一个扇区来进行访问,有可能是一次访问多个扇区。
把盘片想象成为线性结构:
那么LBA和CHS是如何进行转换的呢?
下面进行一个简单的举例:
假设总共有4000个扇区,4个扇面(不考虑正反面),一个扇面有20个磁道(一个磁道有50个扇区),
LBA = 3234
H(磁面):3 <= 3234/1000
C(磁道):11 <= 3234 % 1000 / 20
S(扇区):14 <= 3234 % 1000 % 20
问:为什么Super Block要设计在组内,而不是组外,既然管理的是整个分区,不应该是设计在组外更加合适吗?
答:Super Block分布在多个组中是为了多份备份,防止Super Block文件损毁时整个分区都将无法使用。
注意:不是所有块组中都有Super Block,可能只有两三个,毕竟存在多份的意义就是为了备份而已,没必要每个块组都进行存储,况且每个块组都存储一份Super Block的话也会对修改文件的效率有很大的影响。
问:一个inode(文件,属性)如何和属于自己的内容关联起来呢?
答:
struct inode { //文件的所有属性 blocks[15]; //0~11保存的就是该文件对应的Data blocks编号 //12~14指向一个Data blocks,但是这个Data blocks并不保存有效数据,而保存该文件所使用的其它块的编号 }
只要我们能够找到文件的inode就能找到文件的内容和属性。
问:文件名是如何和文件的inode对应起来的呢?
答:文件名也算文件的属性,但是某个特定的文件的inode里并不保存该文件的文件名,但是Linux底层实际都是通过inode编号标识文件的,那么inode是如何找到的呢?我们前面已经知道,目录也是文件,目录的数据块中存储的就是该目录下文件名和inode的映射关系(文件名是key,inode是value,这也说明了为什么不能在一个目录下多个同名文件的原因)。这也印证了为什么创建一个文件需要我们对该目录具有w权限,查看一个文件名需要我们对该目录具有r权限。
创建一个文件的本质:
OS在创建一个文件时,先在特定的块组中查找inode Bitmap,找到一个未被使用的inode,将其比特位由0置1,然后将新创建的文件属性写到对应的inode Table中,可以将struct inode的blocks数组全部置为0,当我们向文件中写入内容时,就先通过Block bitmap查看哪个Data blocks未被使用,然后在写入文件的同时,将未被使用的Data blocks编号写到struct inode的blocks数组中同时修改Block bitmap由0置1。当然,最关键的是:将文件名和对应的inode写入到创建文件所处的目录文件的数据块(Data blocks)中。
删除一个文件的本质:
1、删除映射关系:先知道该目录文件的文件名,然后根据文件名找到该目录文件的inode,然后通过inode找到该目录文件的数据块(Data blocks),然后将该数据块中存储的要删除的文件名和inode的映射关系删除掉
2、修改inode Bitmap和Block Bitmap:通过要删除的文件的inode找到使用的blocks,然后将对应的Block Bitmap由1置0即可,然后将对应的inode Bitmap中相对应的inode比特位由1置0。(这个修改位图发生在1中删除映射关系之前的)
相关扩展:
问:我们知道自己当前所处的目录,就能知道目录的inode吗?
答:不能。以下面的路径进行举例:
我们知道当前所处的目录即2022_10_28,如果我们想要知道当前目录2022_10_28的inode,就必须来到linux_for_practice目录,linux_for_practice目录中的Data blocks块中保存的文件内容就是2022_10_28的目录名和inode映射。
所以我们就能够推导处ls指令底层到底做的是什么:比如在上面的路径下执行ls指令,首先先通过…上级目录的Data Blocks找到当前目录即2022_10_28的inode,然后通过inode找到当前目录的Data Blocks,进而找到文件名和inode的映射关系,然后将文件名输出到stdout上即可。
命令:stat filename
三个时间:
- Access 最后访问时间
- Modify 文件内容最后修改时间
- Change 属性最后修改时间
指令:ln -s 被链接文件名 链接文件文件名
示例:
作用:软链接经常会被作为一种类似Windows下的快捷方式的形式帮助我们快速的执行某个文件。
内容:保存的是指向的文件的所在路径。
指令:ln 被链接文件名 链接文件名
示例:
作用:在Linux指定的目录下,给指定的文件新增文件名和inode编号的映射关系。一般用来进行路径间的切换。
硬链接数记录了有几个硬链接本质就是该文件inode的一个计数器count,标识有几个文件名和该文件的inode建立了映射关系。简而言之,就是有几个文件名指向该文件的inode。
示例:
问:为什么目录文件(dir)创建出来硬链接数是2,普通文件创建出来硬链接数是1?
答:普通文件创建出来硬链接数是1的原因很好理解,因为普通问及那的文件名本身就映射着该文件inode,且只有一份。
目录文件在该目录内默认存在一个文件名叫.
的隐藏文件表示当前目录,该文件也是该目录所对应目录文件的映射。且如果该目录文件内还有目录文件(indir),那么在indir路径的时候,会存在一个文件名叫..
的隐藏文件表示上级目录,该文件就是dir目录文件的硬链接文件,此时硬链接数还要加1,变成3。
软链接是独立的文件,有自己的inode编号,硬链接不是一个独立的文件,和目标文件使用的是同一个inode。
问:软链接很 明显是占据磁盘空间的,那么硬链接是否占据磁盘空间呢?
答:硬链接是不占据磁盘空间的,但既然如此,那为什么我们使用ls -h
指令时发现计算得出所占磁盘空间的总数也是包含硬链接文件的呢?因为ls -h
这条命令进行统计文件总大小的时候,并不是从磁盘进行统计的,而是根据文件属性的大小进行相加然后得到的最后的结果。
unlink 链接文件名
:unlink的作用其实和rm是类似的,作用结果也是完全一样的,但是我们一般会习惯使用unlink来删除链接文件,而不使用rm来进行删除。
举例如下:
unlink myhard.txt
unlink mysoft.txt
mymath.h文件:
#pragma once
#include
#include
//[from, top] 累加 -> result -> return
extern int addToVal(int from, int to);
mymath.c文件
#include"mymath.h"
int addToVal(int from, int to)
{
assert(from <= to);
int result = 0;
for(int i = from; i <= to; i++)
{
result += i;
}
return result;
}
myprint.c文件
#include"myprint.h"
void Print(const char* msg)
{
printf("%s:%lld\n", msg, (long long)time(NULL));
}
myprint.h文件:
#pragma once
#include
#include
extern void Print(const char* msg);
形成静态库:
makefile文件
命令形式:
ar -rc xxxx.a xx1.o xx2.o
(xxxx.a的格式必须是libxxx.a的格式)ar是gnu归档工具,rc表示(replace and create)
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
.PHONY:clean
clean:
rm -f *.o *.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
.PHONY: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
使用Makefile截图:
shared: 表示生成共享库格式
fPIC:产生位置无关码(position independent code)
静态库:.o文件的代码不能在内存的任意位置去加载,必须拷贝进我们的程序中以地址空间绝对地址的方案呈现在进程层面上让系统去调用,这叫与地址有关码。
动态库:.o代码代码可以加载到内存空间的任何位置,都能够让我们的程序调用并执行它。采用的是起始地址+偏移量的方式即相对地址的方案。
库名规则:libxxx.so
Makefile文件:
libmymath.so:mymath.o myprint.o
gcc -shared -o libmymath.so mymath.o myprint.o
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
.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 -rf *.o *.so lib-dyl
使用图示:
Makefile文件:
.PHONY:all
all:libmymath.so libmymath.a
libmymath.so:mymath.o myprint.o
gcc -shared -o libmymath.so mymath.o myprint.o
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
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
.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
使用图示:
头文件的搜索路径:
“” or <>
1、在当前路径下查找头文件
2、在系统头文件路径下查找头文件(/usr/include/)
指定头文件的搜索路径:gcc Test.c -I ./lib-static/include/
库的搜索路径:
/lib64/路径下。
将我们的头文件和库添加到系统的库中去(不推荐,会污染库)
将头文件和静态库文件拷贝到系统的头文件路径和系统的静态库路径下(这个过程我们也一般叫作库的安装):
此时的报错就不是头文件找不到了,而是方法没有定义了,因为gcc/g++默认知道使用/lib64/里面的标准库,但是我们刚cp进去的库文件,系统是不知道的(因为他们不是标准库),我们需要在gcc选项中自己手动加上:
gcc 源文件 -l库名
//此处需要注意的是,库名是去掉前面的lib和后面的.a后缀的,比如这个地方我们的库名就是mymath(原名:libmymath.a)
//-l 指明要链接的第三方库的名称
此时能够说明我们链接添加的是静态库的个具体表现就是当我们删掉我们添加到库中的头文件和库文件(卸载库)后,我们刚刚形成的a.out文件依然能够继续执行:
指定头文件搜索路径和库文件搜索路径
gcc Test.c -I ./lib-static/include/
gcc Test.c -I ./lib-static/include/ -L ./lib-static/lib/ -lmymath
//注意:-I:头文件搜索的路径 -L:库所在的路径 -l:后面紧跟在L指定的路径下我们想要链接的确切的库的名字(当然,可以加空格,可以不加,不推荐加)
将我们的头文件和库拷贝到系统的库中去(动态库放到/lib64/或者/usr/local/lib64/目录下,头文件还是/usr/include/下,不推荐,会污染库)
指定头文件搜索路径和库文件搜索路径
gcc Test.c -I lib-dyl/include/ -L lib-dyl/lib/ -l mymath
使用上面的方式生成a.out可执行程序后发现无法执行,此时我们通过ldd a.out
查看一下程序依赖的库:
发现依赖的动态库没有查找到。
此时我们思考一下为什么,我们之前用的选项-I -L -l都是告诉的gcc编译器在哪里链接什么库,并且gcc编译器已经帮助我们生成了可执行程序,既然已经生成了可执行程序,那么就和gcc编译器已经没有任何关系了,程序一旦(./a.out)运行起来就变成了进程,此时没有人告诉这个进程该去哪链接库,所以在编译生成可执行程序时告诉编译器去哪链接库生成可执行程序和告诉进程去哪里进行链接库是不一样的,这里也体现出了动态库和静态库链接的不同,静态库一经链接生成可执行程序在运行可执行程序的时候就不需要再进行链接了(静态库在形成可执行程序时就把需要的静态库拷贝到代码中了,所以运行时就不需要链接任何库了),但是动态库即使生成了可执行程序在运行可执行程序的时候依旧需要有人去告诉它去哪链接库(程序和动态库是分开加载的)。
那么,如何使进程找到动态库呢?
动态库拷贝到系统路径下
通过导入环境变量的方式 – 程序运行的时候,会在环境变量中查找自己需要的动态库路径 —— LD_LIBRARY_PATH
(关机退出后再重新进入就不行了,只在当前命令行中有效)
最开始的环境变量:
添加后:
此时再通过ldd指令查看a.out文件的依赖路径:
此时程序就可以运行成功了
系统配置文件(/etc/ld.so.conf.d/
该路径表明了系统在扫描动态库路径时出了在系统路径下进行扫描外,还会在该路径的文件中存储的路径下进行扫描)(关掉之后再打开依旧有效)
注意:该路径下存储的就是路径。下面进行查看:
在/etc/ld.so.conf.d/
路径下新建一个文件名叫myPath并向其写入动态库所在路径(都需要sudo来提升权限)
使用ldd a.out
命令发现依然显示not found
此时执行下面的命令需要让刚才添加的路径生效:
sudo ldconfig
再次执行ldd a.out
进行查看:
此时程序可以正常执行了。
其它方式
在系统路径下建立一个我们自己库文件的软链接
此时程序可以正常执行:
此时将a.out删除之后再通过gcc生成可执行程序的时候就不需要指明库在哪里了。
此时其它的进程也可以将libmymath.so映射进自己struct mm_struct的共享区中,需要注意的是:此时动态库只需要在内存中有一份就可以。
与静态库的不同:
静态库一般会在编译的时候将代码拷贝到代码区中,而动态库是在运行的时候将内存中的动态库代码通过页表映射到共享区中。
ncurses —— Linux下的图形界面库,安装指令:sudo yum install -y ncurses-devel
boost,安装指令:sudo yum install -y boost-devel