技术支持 鼎维重庆网站建设专家网页制作作业网站
- 作者: 五速梦信息网
- 时间: 2026年03月21日 10:48
当前位置: 首页 > news >正文
技术支持 鼎维重庆网站建设专家,网页制作作业网站,wordpress 中文文件重命名,自考大专报名官网入口预备知识 文件 属性#xff08;本质上也是数据#xff09;内容#xff1b; 文件的所有操作大致有两种#xff0c;对内容的操作#xff0c;和对属性的操作#xff1b; 文件在磁盘中放置#xff0c;磁盘是硬件#xff0c;只有操作系统可以真正的访问磁盘#xff1b;C\C… 预备知识 文件 属性本质上也是数据内容 文件的所有操作大致有两种对内容的操作和对属性的操作 文件在磁盘中放置磁盘是硬件只有操作系统可以真正的访问磁盘C\C程序要读写需要先变为进程而文件在磁盘上放着我们访问文件需要先写代码fopen等然后编译生成exe文件运行后访问文件所以访问文件本质上是进程在访问文件进程访问文件是需要通过接口访问之前接触的接口是C\C语言层面的接口但是访问文件相当于要向硬件写入所以只有操作系统通过驱动程序才能向硬件写入 如果普通用户也想写入呢 就必须让操作系统提供接口才能实现普通用户的写入 于是就有了文件类的系统调用接口 为什么语言上有文件操作的调用接口函数呢 1、因为操作系统的文件操作调用接口使用比较难所以在语言层面上进行了封装所以导致了不同的语言有不同的语言级别的文件访问接口都不一样但是都因为封装的是系统调用接口所以就需要学习操作系统的文件接口底层都是调用操作系统的接口操作系统只有一套 2、跨平台代码可以在Linux、windows等平台跑所有的语言基本都是跨平台的如果C/C不提供文件的系统接口的封装那么所有的访问文件的操作都必须直接使用操作系统的接口而用语言的客户也需要访问文件一旦使用系统接口编写文件代码这份代码无法在其他平台直接运行了所以代码就不具备跨平台性了将所有的平台的代码都实现一遍条件编译动态裁剪 显示器是硬件printf向显示器打印也是一种写入与磁盘写入文件没有本质的区别 Linux认为一切皆是文件 对于文件的操作而言就是文件的读和写 对于显示器而言printf/cout就是一种写入 键盘输入相当于给了显示器一份进行显示scanf/cin 对应着是一种读 站在所写的程序的角度要加载到内存cin就相当于input显示器就相当于output 普通文件 - fopen/fread - 进程内部内存- fwrite - 文件中 普通文件到进程内部相当于input从进程内部到文件中相当于output 所以文件的概念就是站在系统的角度能够被input读取或者能够output写出的设备就叫做文件也就是说 狭义的文件是磁盘中的文件 广义上的文件显示器、键盘、网卡、声卡、显卡、磁盘、几乎所有的外设都可以称之为文件 键盘显示器也可以看做文件 C语言的接口 r:只读 r读写 w先清空文件再写入 w读写如果不存在则会创建 a写到文件的结尾向文件中追加新增内容 a 代码 #include stdio.h #include unistd.hint main() {FILE *fp fopen(log.txt, w); //在当前文件夹下没有log.txt而w会创建那么文件会在哪里创建// 当前路径一般是可执行程序在哪里创建的文件就在哪里// if (fp NULL){perror(fopen);return 1;}//进行文件操作fclose(fp);return 0; } 在当前文件夹下没有log.txt而w会创建那么文件会在哪里创建 在当前路径下一般是可执行程序在哪里创建的文件就在哪里 是进程运行后创建的当一个进程运行起来的时候每一个进程都会记录自己所处的工作路径底层会将cwb与log.txt拼接起来生成最终的路径 #include stdio.h #include unistd.hint main() {FILE *fp fopen(log.txt, w); if (fp NULL){perror(fopen);return 1;}//进行文件操作const char *s1 hello fwrite\n;//\n的作用是在文件中换行输入fwrite(s1, strlen(s1), 1, fp);const char *s2 hello fprintf\n;fprintf(fp, %s, s2);const char *s3 hello fputs\n;fputs(s3, fp);fclose(fp);return 0; } 输出结果 fwrite函数中需要使用 strlen(s1)1 来将\0写进文件中吗 不需要因为 \0 仅仅是C语言的规定当加1后会出现乱码 因为普通文件对 \0 无法解释文件只需要保存有效数据 w先清空文件再写入 命令行echo goodball log.txt 向文件中写入内容goodball log.txt 将文件中的内容清空 按行读取内容 #include stdio.hint main() {FILE *fp fopen(log.txt, r); if (fp NULL){perror(fopen);return 1;}//按行读取const line[64];//fgets 会自动在字符结尾添加\0while (fgets(line, sizeof(line), fp) ! NULL){printf(%s,line);fprintf(stdout, %s, line);}return 0; } 此时可以显示出文件log.txt 的内容 修改代码为 #include stdio.hint main(int argc,char *argv[]) {if (argc ! 2){printf(argc error!\n);return 1;}FILE *fp fopen(argv[1], r); if (fp NULL){perror(fopen);return 1;}//按行读取const line[64];//fgets 会自动在字符结尾添加\0while (fgets(line, sizeof(line), fp) ! NULL){printf(%s,line);fprintf(stdout, %s, line);}return 0; } 此时就写出了一个cat命令查看文件中的内容 C\C程序会默认打开三个数据流 标准输入键盘 extern FILE *stdin; 标准输出显示器 extern FILE *stdout 标准错误显示器 extern FILE *stderr 标准输入输出流都是文件指针 系统接口的使用 库函数的使用一定会调用系统接口 四个系统接口open、close、read、write OPEN 使用前两个接口较多 const char *pathname打开文件的路径文件名 int flags代表一些选项O_APPEND追加O_CREAT如果文件不存在则会创建O_TRUNC清空再使用OPEN时必须要带O_RDONLY只读、O_WRONLY只写、O_RDWR读和写其中的一个 如何给函数传递标志位 上边所有的选项都是宏定义在C中想要传标记位布尔型/整型只能传一种选项表示一种状态如果想要传递多个可以考虑一个整型有32位于是多个标记位可以通过每个位进行传递 位图 #include stdio.h #include unistd.h #include string.h//0000 0000 用整形中的不同的bit位就可以表示一种状态 #define ONE 0x1 //0000 0001 #define TWO 0x2 //0000 0010 #define THREE 0x4 //0000 0100void show(int flags) {if(flags ONE) printf(hello one\n);if (flags TWO) printf(hello two\n);if (flags THREE) printf(hello three\n); }int main() {show(ONE);printf(———————————\n);show(TWO);printf(———————————\n);show(ONE | TWO);printf(———————————\n);show(ONE | TWO | THREE);printf(———————————\n);return 0; } 运行结果 操作系统中传递标志位的方案就是按照代码所述进行传递的也就是flags 返回值 #include stdio.h #include unistd.h #include string.h #include sys/types.h #include sys/stat.h #include fcnt.hint main() {int fd open(log.txt, O_WRONLY);if (fd 0){perror(open);return 1;}//open successprintf(open success,fd:%d \n, fd);return 0; } 此时会报错没有log.txt 文件因为系统接口不会帮你去自动创建文件需要使用对应的标志位标志位操作如函数所示 #include stdio.h #include unistd.h #include string.h #include sys/types.h #include sys/stat.h #include fcnt.hint main() {int fd open(log.txt, O_WRONLY|O_CREAT);if (fd 0){perror(open);return 1;}//open successprintf(open success,fd:%d \n, fd);return 0; } 但此时的运行结果正常但是文件的权限如图 在需要创建文件的时候不能使用第一个open函数应该使用第二个函数一般仅仅需要读取的时候会用到第一个接口当需要创建文件的时候就需要使用第二个接口来给文件添加权限 用法 #include stdio.h #include unistd.h #include string.h #include sys/types.h #include sys/stat.h #include fcnt.hint main() {int fd open(log.txt, O_WRONLY|O_CREAT,0666);// 0666表示 rw-rw-rw-if (fd 0){perror(open);return 1;}//open successprintf(open success,fd:%d \n, fd);return 0; } 但此时的运行创建的文件权限并不是 rw-rw-rw- 而是rw-rw-r– 是因为在系统中存在umask这时存在一个接口可以在进程的上下文中设置属于进程的umask #include sys/types.h #include sys/stat.h mode_t umask(mode_t mask); #include stdio.h #include unistd.h #include string.h #include sys/types.h #include sys/stat.h #include fcnt.hint main() {umask(0);int fd open(log.txt, O_WRONLY|O_CREAT,0666);// 0666表示 rw-rw-rw-if (fd 0){perror(open);return 1;}//open successprintf(open success,fd:%d \n, fd);close(fd);//关闭return 0; } 通过umask操作后既可以创建好666权限的log.txt文件 如果文件已经存在的话就用两个参数的函数 write函数 #include stdio.h #include unistd.h #include string.h #include sys/types.h #include sys/stat.h #include fcnt.hint main() {umask(0);//int fd open(log.txt, O_WRONLY|O_CREAT,0666);// 0666表示 rw-rw-rw-int fd open(log.txt, O_WRONLY);if (fd 0){perror(open);return 1;}const char *s hello write;write(fd, s, strlen(s));//open successprintf(open success,fd:%d \n, fd);return 0; } 此时文件中写入hello write但是当再次使用write向这个文件写aa文件中的内容会变成aallo write如果想打开文件的时候先清空需要在open中使用标志位O_TRUNC文件中就会变为aa标志位O_APPEND 追加 read 返回值读到的字节数 #include stdio.h #include unistd.h #include string.h #include sys/types.h #include sys/stat.h #include fcnt.hint main() {int fd open(log.txt, O_RDONLY);if (fd 0){perror(open);return 1;}printf(open success,fd:%d \n, fd);char buffer[64];memset(buffer, \0,sizeof(buffer));read(fd, buffer, sizeof(buffer));printf(%s, buffer);return 0; } 这就是read文件 文件描述符分析接口的细节引入fd 如何深入理解上边的代码 当打开一个文件的时候文件描述符fd的值为3为什么没有0,1,2为什么是连续的 0对应标准输入1对应标准输出2对应标准错误 int main() {fprintf(stdout, hello stdout\n);const char s hello 1\n;write(1, s, strlen);return 0; } 此代码将两行都直接输出到显示器中 int main() {int a 10;fscanf(stdin, %d, a);printf(%d\n, a);return 0; } 此时可以直接输入一个数此数字也将打印出 int main() {char input[16];ssize_t s read(0, input, sizeof(input));if (s 0){input[s] \0;printf(%s\n, input);}return 0; } 这个代码与上面的代码效果一样 FILE 文件指针 FILE是一个结构体C标准库设计的FILE结构体一般内部会有多种成员C文件库函数内部一定要调用系统调用在系统角度只认识fd不认识FILE是什么所以FILE结构体中一定封装了fd 而stdin、stdout、stderr是FILE 的所以是FILE结构体内部肯定有fd 周边文件fd的理解fd和FILEfd分配规则fd和重定向缓冲区 所以fd是什么呢 进程要访问文件必须要先打开文件 一个进程可以打开多个文件 一般而言进程打开的文件可以是1n的 一个文件要想被访问需要加载到内存中才能被访问 如果是多个进程都打开自己的文件呢那么系统中会出现大量的被打开的文件 所以操作系统需要把大量的文件管理起来 在内核中操作系统内部要管理每一个被打开的文件构建结构体 struct file { struct file *next; struct file *prev; //包含了一个被打开的文件的几乎所有的内容不仅仅包含属性包括文件的权限、 //链接信息等 } 打开一个文件就创建struct file 的对象充当一个被打开的文件 所有打开的文件通过这样的链表来管理所以当进程和文件的对应关系就是只要找到链表的头部就可以找到所有的文件 fd的本质就是一个数组的下标 文件分为两种被打开的文件内存文件和没有被打开的文件假设有100000个文件只有500个被打开其他的文件都在磁盘中等待被打开内容属性被打开的文件是被进程打开是通过进程使用open系统调用调用系统打开文件 files_struct与其左侧的部分为进程管理右侧的部分是文件管理 当使用fopen函数时会调用open系统调用接口然后得到一个文件描述符fd然后将fd封装为FILE最后一FILE * 的方式返回fopen函数 fwrite函数传递进来一个FILE * 内部有fd并且内部封装这write就会将fd传到write中最后操作系统内部能找到进程的task_struct然后找到进程内部的*fs指针就找到files_struct,然后找到fd_array[]配合传进的fd就找到了struct_file 内存文件就被找到了 文件描述符的分配规则 #include stdio.h #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.hint main() {int fd open(log.txt, O_WRONLY | O_CREAT | O_TRUNC0666);if (fd 0){perror(open);return 1;}printf(fd:%d\n, fd);close(fd);return 0; } 此时运行输出的为3因为0、1、2被占用了 #include stdio.h #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.hint main() {close(0);int fd open(log.txt, O_WRONLY | O_CREAT | O_TRUNC);if (fd 0){perror(open);return 1;}printf(fd:%d\n, fd);close(fd);return 0; } 此时输出的是0fd的分配规则是前边关掉0后边就会分配0前边关掉2就会分配2 当操作系统打开文件时需要先在内存中打开这个文件打开文件后有了进程对象就要和这个进程关联起来在对应的fd_arrray[]中搜索哪个数字没有被占用会遍历整个fd_array找到最小的没有被占用的文件描述符 例1 #include stdio.h #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.hint main() {close(1);int fd open(log.txt, O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd 0){perror(open);return 1;}printf(fd:%d\n, fd);return 0; } 此时将代码close(fd)删除显示器不显示结果打开log.txt文件后文件中显示内容 fd1 例2 #include stdio.h #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.hint main() {close(1);int fd open(log.txt, O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd 0){perror(open);return 1;}printf(fd:%d\n, fd);fflush(stdout);close(fd);return 0; } 此时的运行结果与上边的例1运行结果一样 在close(1)后边的内容本来是应该打印到显示器中的但是都打印到了文件中但是将close(1)删除后内容就会被打印到显示器中这就叫做除数重定向 重定向 输出重定向原理 在打开文件会先创建log.txt文件的struct_file对象如果此时将1关闭将是将进程和这个文件的关联关系去掉那么此时open开始遍历file_struct于是此时1就指向了log.txt而C语言层面stdout只认识数字1于是就发生了上面的情况 输入重定向 #include stdio.h #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.hint main() {int fd open(log.txt, O_RDONLY);if (fd 0){perror(open);return 1;}printf(fd:%d\n, fd);char buffer[64];fgets(buffer, sizeof buffer, stdin);printf(%s\n, buffer);return 0; } 此时会将文件log.txt文件中的第一行打印出来因为gets函数只打印第一行 本来应该从键盘中读取的内容现在从文件中读取了这就是输入重定向 追加重定向 #include stdio.h #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.hint main() {close(1);int fd open(log.txt, O_RDONLY|O_APPEND|O_CREAT);if (fd 0){perror(open);return 1;}fprintf(stdout,hello baby!\n);return 0; } 输入重定向会将文件中的内容清空再向文件中打印将open的方式改变一下就可以实现追加重定向但是此时还有一些问题在重定向时需要关闭0,1,2再打开文件描述符才有的事实上重定向并不是这么实现的 重定向的系统调用 dup dup2是将oldfd拷贝给newfd拷贝的东西是files_struct结构体中的指针就是将3的内容拷贝到1中也就是让原来指向3的指向1 代码 #include stdio.h #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.hint main(int argc,char argv[]) {if (argc ! 2){return 2;}int fd open(log.txt, O_WRONLY | O_CREAT | OTRUNC);if (fd 0){perror(open);return 1;}dup2(fd, 1);fprintf(stdout,%s\n, argv[1]);return 0; } 此为输出重定向的使用输入和追加重定向方法与前边一致 如何理解一切皆文件 Linux的设计哲学体现在软件设计层面 如何用C语言实现面向对象甚至是运行时的多态 类包含成员属性和成员方法 C语言中struct结构体是面向对象语言的起点 struct不能包含函数 但是如果必须在struct中包含成员方法 但是可以包含函数指针这时就可以使用C语言封装一个类 首先底层不同的硬件对应着不同的操作方法但是这些硬件都是外设所以每一个设备的核心访问函数都可以是read、write、I/O所以所有的设备都可以有自己的read和write但是代码的实现一定是不一样的 此时就相当于直接使用函数指针调用不同外设对应的读写方法操作系统只通过struct file即可对应到各种硬件这样就没有硬件的差别了所有的外设都是struct file 这就是Linux的一切皆文件这种设计方案叫做VFS虚拟文件系统 缓冲区 什么是缓冲区 是一段内存空间 用户层缓冲区char buffer[64]scanf(bufer);为了方便 为什么要有缓冲区 写透模式WT相当于寄件人直接将包裹送给收件人成本高慢 写回模式WB通过顺丰快递邮寄包裹 顺丰快递相当于缓冲区快主要为了提高用户的相应速度 常见的缓冲区刷新策略 立即刷新 行刷新(行缓冲)\n将\n之前的数据刷新出去 满刷新(全缓冲)将缓冲区写满了才能刷新出去 特殊情况 用户强制刷新fflush 进程退出 缓冲策略一般特殊 缓冲区在哪里 #include stdio.h #include string.h #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.hint main() {//C语言提供的printf(hello printf\n);fprintf(stdout, hello fprintf\n);const char *s hello fputs\n;fputs(s, stdout);//操作系统提供的const char *ss hello write\n;write(1, ss, strlen(ss));fork();return 0; } 运行结果 当使用指令 ./myfile log.txt时log.txt中的内容 如果将代码中的fork函数注释后那log.txt文件中的内容与第一个一样 这种现象与fork有关 可以看见C语言提供的部分被打印了两次 关于缓冲区的认识 一般而言行缓冲的设备文件一般是显示器 全缓冲的设备文件一般是磁盘文件 所有的设备都倾向于全缓冲因为缓冲区满了才刷新需要更少次的外设的访问也就提高了效率和外部设备IO的时候数据的大小不是最主要的矛盾和外设的IO过程是最耗费时间的其他刷新策略是结合具体的情况做的妥协 显示器直接给用户看的一方面要照顾效率一方面要照顾用户体验极端情况是可以自定义规则的 解决上面的问题 CIO接口打印了两次 系统接口只打印了一次 首先fork上面的代码执行完了并不代表数据已经刷新了 缓冲区是由C标准库提供的 fputs是通过进程写到C标准库维护的缓冲区中然后再通过C标准库将缓冲区的内容刷新到操作系统中而刷新方法就是调用write接口而调用write接口就会直接将数据写给操作系统不会写给缓冲区 在代码中C语言提供的部分如果想显示器打印的话刷新策略是行刷新那么最后执行fork的时候一定是函数执行完了并且数据已经被刷新了fork无意义 如果对应的程序进行了重定向本质是本来应该向显示器打印变成了要想磁盘文件打印隐形的刷新策略变成了全缓冲那么\n就没有意义了fork的时候一定是函数一定执行完了但是数据还没有刷新在当前进程对应的C标准库中的缓冲区中这部分数据属于父进程的数据fork之后父子进程各自退出而刷新就是写入的过程在退出时发生写时拷贝所以就会出现两份的数据 C标准库给我们提供的用户级缓冲区还存在内核级缓冲区 #include stdio.h #include string.h #include unistd.hint main() {//C语言提供的printf(hello printf\n);fprintf(stdout, hello fprintf\n);const char *s hello fputs\n;fputs(s, stdout);//操作系统提供的const char *ss hello write\n;write(1, ss, strlen(ss));fflush(stdout);fork();return 0; } 在fork之前加一个fflush函数此时执行命令 ./myfile log.txt 后log.txt中的文件与直接运行文件输出的结果一样 fflush是C语言提供的接口所以刷新的时候是将数据直接刷新到缓冲区中在fork之后数据已经没了所以就没有了还有在fflush的时候只刷新了stdout那只传入stdout就能刷新到缓冲区吗C语言中打开文件调用的接口是fopenfopen对应的的返回值是FILE *struct FILEFILE是结构体内部封装了fd但不是只封装了fd除了fd还有该文件fd对应的语言层的缓冲区结构 C语言中打开的file叫做文件流cout cin 是类类中包含fd、定义buffer是操作符使用operator对进行重载将数据打印到类内的buffer中然后定期去刷新 设计用户缓冲区 代码 #include stdio.h #include string.h #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.h #include assert.h #include stdlib.h#define NUM 1024struct MyFILE {int fd;char buffer[NUM];int end;//当前缓冲区的结尾 };typedef struct MyFILE_ MyFILE;MyFILE *fopen_(const char *pathname, const char *mode) {assert(pathname);assert(mode);//pathname和mode不能为空MyFILE fp NULL;if (strcmp(mode, r) 0){}else if (strcmp(mode, r) 0){}else if (strcmp(mode, w) 0){int fd open(pathname, O_WRONLY | O_TRUNC | O_CREAT, 0666);if (fd 0)//如果申请或者打开失败了就会返回fpNULL//如果成功打开并申请空间fp返回申请的空间地址{fp (MyFILE)malloc(sizeof(MyFILE));memset(fp, 0, sizeof(MyFILE));//将缓冲区清空fp-fd fd;}}else if (strcmp(mode, w) 0){}else if (strcmp(mode, a) 0){}else if (strcmp(mode, a) 0){}else{//什么都不做}return fp; }//向缓冲区中写入message的数据 void fputs(const char *message, MyFILE *fp) {assert(message);assert(fp);strcpy(fp-buffer fp-end,message);//strcpy函数会自己在后边加\0;fp-end strlen(message);//向标准输入、标准输出、标准错误中刷新时需要调整策略if (fp-fd 0){//标准输入}else if (fp-fd 1){//标准输出//行刷新if (fp-buffer[fp-end - 1] \n){fprintf(stderr, fflush:%s, fp-buffer);write(fp-fd, fp-buffer, fp-end);fp-end 0;}}else if (fp-fd 2){//标准错误}else{//其他文件}}fflush(MyFILE *fp) {assert(fp);if (fp-end ! 0)//判断缓冲区中有内容才刷新{//其实是将数据写到了内核write(fp-fd, fp-buffer, fp-end);syncfs(fp-fd);//将数据写入到磁盘中fp-end 0;} }fclose(MyFILE *fp) {assert(fp);fflush(fp);close(fp-fd);free(fp); }int main() {MyFILE *fp fopen(./log.txt, w);if (fp NULL){printf(open file error);return 0;}fputs(1hello world, fp);fputs(:2hello world\n, fp);fputs(3hello world, fp);fputs(4hello world\n, fp);fclose(fp);return 0; } 此时最终的打印结果是1hello world 2:hello world 3:hello world 4:hello world 遇到\n才刷新 而刷新策略是用户通过执行C标准库中的代码逻辑来执行刷新动作 效率提高体现在因为C提供了缓冲区那么我们就通过策略减少了IO的执行次数而不是数据量 修改myshell支持重定向 #include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/wait.h #include sys/types.h #include sys/stat.h #include fcntl.h #include assert.h#define NUM 1024 #define SIZE 32 #define SEP //保存打散之后的命令行字符串 char *g_argv[SIZE]; //保存完整的命令行字符串 char cmd_line[NUM];//定义一个大小是1024的缓冲区#define INPUT_REDIR 1 #define OUTPUT_REDIR 2 #define APPEND_REDIR 3 #define NONE_REDIR 0int redir_statusNONE_REDIR;char *CheckRedit(char *start) {assert(start);char *end start strlen(start) - 1;//此时直接指向字符串最后一个有效字符while (end start){if (end ){if ((end-1) ){redir_status APPEND_REDIR;*(end - 1) \0;end;break;}redir_status OUTPUT_REDIR;*end \0;end;break;}else if (*end ){redir_status INPUT_REDIR;*end \0;end;break;}else{end–;}}if (end start){return end;//要打开的文件}else{return NULL;} }//shell 运行原理通过让子进程执行命令父进程等待并且解析命令 //除了要执行系统的命令还要执行自己的命令如果自己的命令崩溃那么不会影响父进程使得父进程能够继续解析命令 int main() {//0、命令行解释器一定是一个常驻内存的进程就是不退出while (1){//1、打印出提示信息//[zyl123456host myshell]printf([root我的主机 myshell]# \n);//后边需要加\n 否则数据在缓冲区内fflush(stdout);//在输入命令时总是换行与printf里的内容不在一行memset(cmd_line, \0, sizeof cmd_line);//sizeof可以不带圆括号//2、获取用户的输入输入的是各种命令和选项ls -a -l -i//ls -a -l log.txt//ls -a -l log.txt//ls -a -a log.txtif (fgets(cmd_line, sizeof cmd_line, stdin) NULL){continue;}cmd_line[strlen(cmd_line) - 1] \0;//由于在输入时输入了回车所以在打印的时候也会打印处一行空行//于是通过上边的这行代码将 \n 置为0//分析是否有重定向ls -a -l log.txt 也及时先找到 符号然后将其转化为ls -a -l \0 log.txt//然后将log.txt文件打开进行重定向就可以了char *sep CheckRedit(cmd_line);printf(echo: %s\n, cmd_line);//3、命令行字符串解析将输入的命令 ls -a -l -i 转换成 ls -a -l -ig_argv[0] strtok(cmd_line, SEP);//第一次调用要传入原始字符int index 1;if (strcmp(g_argv[0], ls) 0)//使得ls命令行带有颜色{g_argv[index] –colorauto;}//while 先调用函数然后赋值给g_argv然后while检测g_argv中的值while (g_argv[index] strtok(NULL, SEP));//第二次如果还要解析原始字符串传入NULLfor (index 0; g_argv[index]; index){printf(g_argv[%d]: %s\n, index, g_argv[index]);}//4、内置命令处理让父进程自己执行的额命令叫做内置命令内建命令//本质就是shell中的函数调用//如果没有第四步那么执行任何命令都会运行execvp那么在cd .. 的时候就会出现路径不发生变化的现象//只会使子进程的路径发生变化已运行完就会退出但是pwb打印出来的相当于是父进程的路径没有改变if (strcmp(g_argv[0], cd) 0)//不想让子进程执行希望使父进程执行命令{//chdir函数更改当前的路径if (g_argv[1] ! NULL){chdir(g_argv[1]);}continue;}//5、fork()pid_t id fork();if (id 0){if (sep ! NULL){int fd -1;//说明命令曾经有重定向switch (redir_status){case INPUT_REDIR:fd open(sep, O_WRONLY);dup2(fd, 0);break;case OUTPUT_REDIR:fd open(sep, O_WRONLY | O_TRUNC | O_CREAT, 0666);dup2(fd, 1);break;case APPEND_REDIR:fd open(sep, O_WRONLY | O_APPEND | O_CREAT, 0666);dup2(fd, 1);break;default:pritnf(bug?\n);break;}}//childprintf(下面的功能让子进程进行\n);execvp(g_argv[0], g_argv);exit(1);}int status 0;pid_t ret waitpid(id, status, 0);if (ret 0){printf(exit code: %d\n, WEXITSTATUS(status));}}//return 0; } 前边小问题的解决 close(fd)之后文件没有数据 #include stdio.h #include string.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.hint main() {close(1);int fd open(log.txt, O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd 0){perror(open);return 0;}printf(hello world %d\n,fd);//向stdout 中打印close(fd);return 0; } 此时打开文件log.txt文件没有内容正常是应该有内容的 当在close前边加一个 fflush(stdout) 在运行程序后打开文件log.txt就有了内容 printf相当于数据暂时存放在stdout的缓冲区中如果close(fd)相当于还未刷新就直接关闭了stdout使得数据无法刷新出来 1,2stdout和stderr有什么不同 #include iostream #include stdio.h #include string.h #include sys/types.h #include sys/stat.h #include fcntl.h #include unistd.hint main() {//stdout - 1printf(hello printf 1\n);fprintf(stdout, hello fprintf 1\n);//stderr - 2perror(hello perror 2);const char *s1 hello write 1\n;write(1, s1, strlen(s1));const char *s2 hello write 2\n;write(2, s2, strlen(s2));std::cout hello cout 1 std::endl;std::cerr hello cerr 2 std::endl;return 0; } 直接运行程序的结果 当使用指令 ./myfile log.txt后文件log.txt中的内容为 可见指令的重定向是往1 号文件描述符写入1与2对应的都是显示器文件但他们两个是不同的显示器文件可以认为是同一个显示器文件被打开了两次 错误信息依旧往显示器上打印一般而言如果程序运行有可能有问题的画建议使用stderr打印如果是常规的文本内容的话一般使用stdout打印 可以通过此将错误信息和标准输出分两个文件打印出来也就有了错误日志 可以使用命令 ./myfile good.txt 2 err.txt 命令 ./myfile log.txt 2 1;意思是将1中的内容给2拷贝一份2也就指向了与1相同的位置 自己实现perror void myperror(const char *msg) {fprintf(stderr, %s,%s\n, msg, strerror(errno)); } 理解文件系统 背景知识 1、前边的内容时进程和被打开的文件之间的对应关系那么没有被打开的文件在哪里呢 在磁盘中磁盘级文件 2、磁盘级文件的侧重点 单个文件的角度这个文件在哪里这个文件多大这个文件的其他属性是什么 站在系统的角度一共有多少个文件各自属性在哪里如何快速找还能储存多少个文件如何快速找到指定的文件 如何对磁盘文件进行分门别类的存储用来支持更好的存取 3、磁盘文件了解磁盘是什么 内存 ——掉电易失存储介质 磁盘——永久性存储介质 永久性介质 SSD固态硬盘比磁盘快、U盘、flash卡、光盘、磁带 磁盘是一个外设同时还是计算机中唯一的一个机械设备磁盘很慢CPU纳秒级别、内存微秒级别磁盘是毫秒或秒级别的操作系统有一些提速的方式 4、磁盘的结构 1磁盘的物理结构 磁盘盘片磁头伺服系统音圈马达 一叠光盘就是盘片音圈马达在光盘中间 当通电后盘片会快速旋转大概7200转在盘片转动的同时磁头快速左右摇摆这个摇摆就是寻址而数据就在盘面上并且每一个面都有磁头盘面上会存储数据而计算机只认识二进制本质上存储的是二进制在日常生活中存在两态的是磁铁南极和北极可以将磁盘盘面上想象成很多细小的磁铁颗粒向磁盘写入本质就是改变磁盘上的正负性磁头都是一些电子信号可以在磁盘的一些位置产生放电行为改变磁盘的一些区域的正负性从而将数据向磁盘中写入二进制 磁头和盘面不是挨着的因为挨着就会将磁盘刮花可能会将数据丢失 磁盘寻道的过程可以搜索看网络视频 2磁盘的存储结构 磁道容量比较大使用时一般使用磁道的一部分一小部分叫做磁道的扇区相同半径的多个磁道合起来叫做柱面扇区是磁盘存储的基本单位一般512字节也有4kb的 在物理上如何将数据写入到一个扇区中 首先确认在那个盘面上 然后确认在哪一个磁道柱面上哪一个同心圆上 然后确定在哪一个扇区上 这种寻址方案叫做CHS寻址 如果有了CHS就可以找到任何一个扇区那么整个盘面就都可以找到了 3磁盘的抽象结构 就像磁带一样如果将磁带中的磁条拉出来就是线性的可以将磁盘想象成磁带就可以将一个大磁盘抽象成数组要是访问一个扇区只要知道数组的下标就可以这种寻址方法叫做LBA寻址方式所以在要访问磁盘的时候只需要将LBA的寻址方式转换成CHS方式即可 而操作系统通过LBA去访问 500GB区域太大不方便管理所以需要将其分为几个区域 对磁盘的管理转化成了对一个小分区的管理 而对一个小分区的管理还需要对分区继续做拆分 相当于国家划分为一个个省将省拆分为一个个市再将市划分为县 SuperBlock:不是所有的块组都有文件系统的属性信息表示的是整个分区如图中是100GB的信息而不是所在Block group的信息有多少个块组那些块组已经满了中国有过多少个数据块可以被写有多少个数据块可以被占inode有多少个还有多少个没有被使用以及应用了多少 Data Blocks虽然磁盘的基本单位是扇区但是操作系统文件系统和磁盘进行IO的基本单位是4kb8*512byte块大小所以磁盘一般称为块设备因为512太小了在做数据拷贝的时候可能超过512所以就会导致多次IO进而降低效率如果操作系统使用和磁盘一样的大小万一磁盘的基本大小变了那么操作系统的源代码也需要改变这样就是将硬件和软件进行解耦所以Data Blocks可以想象成有多个4Kb大小的集合Linux在存储时是将文件的额内容和属性分开存储的此部分保存的是文件的内容有多个 inode Tableinode是一个带下为128 字节的空间保存的是对应文件的属性inode Table内保存的是该块组的所有文件的inode的集合需要表示唯一性所以每一个inode块都要有一个inode编号一般而言一个文件一个inode一个inode编号有多个 Block Bitmap假设有10000个blocks就有10000个比特位比特位和特定的block是一一对应的比特位为1代表该block被占用否则表示可用 inode Bitmap假设有10000个inode节点inode Bitmap中就有10000个比特位比特位和特定的inode是一一对应的其中Bitmap中比特位为1代表该inode被占用否则表示可用 Group Descriptor TableGDT代表块组描述符表示的是这个块组多大已经使用了多少有多少个inode已经占用了多少还剩多少一共有多少个block使用了多少 这样就可以让文件的额信息可追溯可管理 将块组分割为上面的内容并且写入了相关的管理数据每一个块组都是如此整个分区就被写入了文件系统信息有可能一个块组什么信息都没有也就是这个块组就是的两个Bitmap全是0这个过程就叫做格式化 一个文件 “只” 对应一个inode属性节点inode编号 一个文件只能有一个block吗 如果一个文件在4kb内那么就对应一个bloick如果文件夹太大有几个G等就会对应多个inode 于是有问题 1、哪些block属于同一个文件 2、找到文件只要找到对应的inode就能找到该文件的额inode属性集合可是文件的内容怎么找 在inode中包含着一个数组他可以包含对应的块的编号先通过inode编号找到对应的inode然后通过block数组找到对应的block 如果文件特别大怎么办 在data block中不是所有的data block只能存文件数据也可以存其他块文件的块号相当于多叉树的形式 inode和文件名 找到文件需要先找到inode编号 - 分区特定的block group - inode - 属性 - 内容 Linux中inode属性里面没有文件名这样的说法 预备知识 1、一个目录下可以保存很多文件但是这些文件没有重复的文件名 2、目录是文件目录要有自己的inode要有自己的data block 3、一个文件的文件名是在他所属目录文件的data block中存储的目录文件的data block中存储的是文件名和inode编号的映射关系互为Key值 此时就可以理解创建文件需要有目录的w权限需要将文件名写入显示文件名与属性需要有r权限有了r权限才能读取目录文件的内容 1、创建文件系统做了什么 根据文件系统找到目录所在的分区然后找到目录所在的块组在inode Bitmap中找到为0的比特位将0置1同时得到inode编号在inodeTable中将新建文件的属性填入然后将从用户层来的文件名和从内核来的inode填到目录文件中去 2、删除文件系统做了什么 找到目录对应的data block通过文件名找到对应的inode然后将inode Bitmap中对应的1置为0并将Block Bitmap对应的1置为0在从目录中把文件名和inode的映射关系去掉 恢复只要找到所删除文件的inode就可以找回文件在Linux中有删除文件的日志可以找到能恢复出来的前提是曾经的金瓯的属性和datablock没有被占用 3、查看文件系统做了什么 ls ll cat ls的时候找到目录找到目录的 inode然后找到datablock将文件名全部找到然后显示 ll找到目录找到目录对应的datablock根据文件名对应的inode找到各自文件的属性然后拼接好显示出来 echo “hello” myfile.c将文件打开然后向文件中写入文件打开对应的file对象就有了然后把数据写到file对象内核中的额缓冲区操作系统定期刷新把数据刷新到文件上就是向磁盘上写了有文件名目录也知道所以就能找到文件的inode就能找到inode对应的属性也就能找到对应的data block就将数据刷到了盘上 cat根据文件名在当前目录下找到inode根据文件的inode在特定的block中找到对应的属性根据属性找到data block然后将其中的数据刷新到内存中然后通过内存刷新到显示器中 inode是固定的data block是固定的就容易出现一个问题当看到还有内存空间但是创建不了新的文件可能是因为没有inode了或者data block没有空间了 软硬连接 ls -li 可以将文件的inode显示出来 ln -s testLink.txt soft.link 此时就建立了一个软连接 ln testLink.txt hard.Link 此时就建立了一个硬连接 现象软连接有自己的inode硬连接的inode与被连接的文件inode相同软连接被链接的文件前边数字是1硬连接被链接的文件前边的数字是2 软硬连接的区别就是有没有独立的inode 软连接有独立的inode意味着软连接是一个独立的文件而硬连接不是一个独立的文件 软连接 特性有独立的inode可以理解为软连接的文件内容指向的是文件的对应的路径 应用如同windows下的快捷方式 硬连接 特性 创建硬连接不是真正的创建新文件用的别人的内容和别人的属性 创建硬连接就是在指定的目录下建立了文件名和指定的inode的映射关系 属性中有一个数字从1建立硬连接后变为2删除被硬连接的文件变为1 这个数字叫做硬链接数 inode怎么知道有多少个文件与自己是关联的inode内部有一个引用计数int count当删除文件的时候并不是把这个文件的inode删除了而是将引用计数–当引用计数为0的时候这个文件才真正的删除 应用 默认创建目录是引用计数是2是由于自己的目录名映射到inode并且自己目录内.文件与其inode映射所以是两次在此目录下再创建一个目录就变为3由于新创建的目录中有..文件上一级路径就可以不进入目录估算出目录中有几个目录 动态库和静态库 1、怎么写库 库里面不能有main函数 mymath.c #include mymath.hint addToTarget(int from, int to) {int sum 0;for (int i from; i to; i){sum i;}return sum; } mymath.h #pragma once#include stdio.hextern int addToTarget(int from, int to); myprint.c #include myprint.hvoid Print(const char *str) {printf(%s[%d]\n, str, (int)time(NULL)); } myprint.h #pragma once#include stdio.h #include time.hextern void Print(const char *str); 静态库 .a 首先将上边两个.c文件编译为.o文件使用命令 ar -rc lib.a mymath.o myprint.o 即可完成静态库的制作 ar是gnu归档工具rc表示replace和create 库的发布一般是一个文件lib中是各种库文件include中是对应的头文件 makfile文件 动态库 .os gcc -fPIC -c mymath.c -o mymath.o gcc -fPIC -c myprint.c -o myprint.o 生成两个与位置无关的二进制文件 这个库形成了它在内存里的任意位置都可以加载 使用命令gcc -shared mymath.o myprint.o -o libhello.so 于是就得到一个libhello.so动态库 执行命令make 生成.o文件后执行make output 生成动态库和静态库 2、库是怎么使用的 静态库 方法一 hello库 头文件gcc的默认搜索路径是/usr/include 库文件的默认搜索路径是/lib64或者/usr 接下来将hello/include 拷贝到头文件的默认搜索路径/usr/include中再将hello/lib拷贝到库文件的默认搜索路径/lib64中 此时直接编译会报错因为自己所写的库是第三方库需要使用命令gcc main.c -lhello; 需要告诉gcc要连接的库是哪一个不建议使用这种方式使用自己写的库 方法二 动态库 在上面动态库生成中生成的两个库文件在同一个文件夹下的时候上面静态库的方法二默认使用的是动态库 如果只有静态库那么gcc只能针对该库进行静态链接 如果动静态库同时存在默认使用的就是动态库 但是存在一个问题 在成功编译后运行文件会报错 但是如果动静态库同时存在当时非要使用静态库呢 在方法二后边加 -static作用是摒弃默认优先使用动态库的原则直接使用静态库的方案 注命令 ldd a.out 查看a.out文件的连接库是哪个 静态库与可执行文件是一体的而动态库是一个独立的文件 地址空间中有一段代码区在使用静态库编译后可执行文件和静态库都通过页表的映射加载到代码区中堆区与栈区有一批共享区当使用动态链接时先把可执行程序加载进内存代码中会包含一些关于库的符号链接当执行可执行程序时遇到了与动态库有关的代码时才会将动态库加载进内存然后通过页表映射到共享区程序运行时会从代码区跳到共享区在跳回去 当把库加载到内存中时如果有其他的进程也想使用这个库就通过自己的页表将其映射到自己的共享区动态库也就是共享库 上边的报错原因是因为只在gcc编译器中输入了动态库的路径但是加载器并不知道动态库在什么位置 方法一、向环境变量中加入库路径 LD_LIBRARY_PATH加载库路径 使用命令 export LD_LIBRARY_PATH$LD_LIBRARY_PATH动态库所在的的路径 方法二、修改配置文件 sudo touch /etc/ld.so.conf.d/xxx.conf sudo vim xxx.conf 将库所在的路径拷贝到此conf文件中即可 方法三 在lib64下建立软链接 为什么要有库 站在使用者角度库的存在可以大大减少开发周期提高软件本身的质量 站在写库的人的角度使用简单并且代码安全 ncurses 字符的界面库 boost 准标准库
- 上一篇: 技术先进的网站建设公网站备案密码修改
- 下一篇: 技术支持 东莞网站建设石材域名哪个网站买最好
相关文章
-
技术先进的网站建设公网站备案密码修改
技术先进的网站建设公网站备案密码修改
- 技术栈
- 2026年03月21日
-
纪检网站建设计划甘肃网站定制开发
纪检网站建设计划甘肃网站定制开发
- 技术栈
- 2026年03月21日
-
纪检网站建设动态主题全网霸屏推广营销系统
纪检网站建设动态主题全网霸屏推广营销系统
- 技术栈
- 2026年03月21日
-
技术支持 东莞网站建设石材域名哪个网站买最好
技术支持 东莞网站建设石材域名哪个网站买最好
- 技术栈
- 2026年03月21日
-
技术支持 如皋网站建设深圳画册设计龙华
技术支持 如皋网站建设深圳画册设计龙华
- 技术栈
- 2026年03月21日
-
技术支持 上海做网站去设计公司还是去企业
技术支持 上海做网站去设计公司还是去企业
- 技术栈
- 2026年03月21日
