当前位置: 首页 > news >正文

【Linux】深入理解文件操作

文章目录

      • 初次谈论文件
      • 重温C语言文件操作
      • 系统文件操作接口
        • open
        • write
        • read
      • 再次谈论文件
        • 文件描述符
        • 文件描述符的分配规则
      • 重定向
        • 什么是重定向
        • 重定向的本质
        • 系统调用接口实现重定向
        • <、>、>>

初次谈论文件

开始之前先谈论一下关于文件的一些共识性问题。

  1. 一个文件可以分为两部分,内容和属性。
  2. 基于上面的认识,空文件也要在磁盘中占据空间,因为空文件的内容为空,但是还有属性在,例如文件的创建时间… 而这部分属性也是要存储的。
  3. 所以对文件的操作就变成了对文件的内容或对文件的属性进行操作。
  4. 如果我们要标识一个文件,必须要通过路径+文件名的方式来唯一标识。
  5. 当我们使用相关方法进行文件操作的时候我们一般只写一个文件名,此时我们并没有指明文件的路径,此时默认是在当前路径(访问文件的进程的当前路径)下进行相关文件操作。
  6. 当我们写完一份代码,代码中有对文件进行操作的内容,当我们把代码编译成可执行文件后,在我们执行这个程序之前,文件操作并没有被执行,只有当我们运行这个程序,程序变成进程之后才会真正执行相应的文件操作,所以对文件操作本质上是进程对文件的操作
  7. 我们无论是以哪种方式访问文件,前提是都要打开这个文件。而打开文件这个动作是谁完成的呢?谁能管理文件的存储谁就能打开,所以是操作系统打开的,或者准确一点,是操作系统收到进程的访问请求时打开的。所以文件操作的本质是进程和被打开文件之间的关系

重温C语言文件操作

我们常说C默认会打开三个输入输出流:stdinstdoutstderr,这点后面还会用到。

C语言中有诸如fopenfclosefprintffscanffwritefread等涉及文件操作的函数方法。

这里就简单回顾一下部分接口。

  1. FILE * fopen ( const char * filename, const char * mode );

    fopen函数可以打开一个文件,参数分别是文件名和打开方式,返回一个FILE指针。

    mode有多种选项,例如r - 只读r+ - 可读可写w - 只写w+ - 可读可写a - 追加a+ - 可追加可读

    除此之外还有二进制读写、文件不存在是否创建文件、文件是否会覆盖重写等细节问题。

  2. int fprintf ( FILE * stream, const char * format, ... );

    fprintf与普通的printf的区别在于printf是默认向stdout中输出打印,而fprintf则可以指定文件。


系统文件操作接口

实际上,我们所用到的无论是C语言,还是python、Java、c++,它们的相关访问文件的接口虽然各有不同,但它们都是对系统提供的文件操作接口的封装。换句话说,各个所提提供的文件方法底层都是封装了相同的系统提供的文件操作接口。

下面就简单学习几种文件操作接口。

因为要查系统调用接口,所以在使用man手册查询的时候要加个2选项:man 2 [name]

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: 追加写O_TRUNC: 清空文件//...
mode: 如果创建文件的话,文件的访问权限返回值:成功:新打开的文件描述符失败:-1

这里提供了两个open函数,差别在最后一个mode参数,mode其实就是创建文件时文件的默认权限,Linux默认是0666,想了解文件权限的小伙伴可以跳转到这个链接:【Linux】对权限的初步理解_LeePlace的博客-CSDN博客。

参数pathname没什么好说的,就是要打开的文件的完整路径,不过只有文件名的话就默认当前路径。

下面着重介绍flags

我们打开文件时是以读的方式还是以写的方式,是以文本文件的形式读写还是以二进制文件,这些信息都需要通过参数来进行信息传递。

flags是一个int整形,拥有32个bit位,我们如果给这32个bit位每一位都赋予一定意义,比如第一个bit为1就是以读的方式打开,第二个bit位为1就是以写的方式打开… 此时每一个bit都是一个标记位,而系统给我们提供了一些宏,比如O_WRONLY可能是1 << 0O_CREAT可能是1 << 1O_TRUNC可能是1 << 2,此时把这三个数按位或就得到一个前三个比特位都是1的flag,表示以只写、文件不存在的时候创建文件、打开时清空文件的方式来打开一个文件。

学过c++访问文件的方式的小伙伴对这种方式肯定不陌生,比如经常用到像ios::in | ios::binary的参数。

open会返回一个int,叫文件描述符,后面再对文件描述符进行更进一步的讨论。

所以我们现在就可以试着用open来打开一个文件:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);close(fd);return 0;
}

image-20230822141447899

因为是以只写的方式打开,文件不存在我们选择创建文件,所以可以成功创建文件,而文件的权限并不是0666,这是因为文件掩码的存在,这里就不多做解释,如果想不受文件掩码的影响可以加一句umask(0)

当然上面还用到了close,这其实就是系统提供的关闭文件的接口,参数是要关闭文件的文件描述符,就不多做介绍了。

如果想以只读的方式打开文件,则是open(FILE_NAME, O_RDONLY);

如果想以追加的方式打开文件,则是open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666);


write

NAMEwrite - write to a file descriptorSYNOPSIS#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);DESCRIPTIONfd: 要写入的文件描述符buf: 一个指针,指向待写入的数据count: 待写入的数据的大小,单位是字节RETURN VALUE成功:返回写入的字节数失败:返回-1,并适当设置errno

所以我们可以试着用write向文件中写入:

#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 1;}int cnt = 5;char outBuffer[64];while(cnt){//将aaaa和cnt序列化成字符串存储到outBuffer中sprintf(outBuffer, "%s:%d\n", "aaaa", cnt--);//将outBuffer的内容写入到文件中write(fd, outBuffer, strlen(outBuffer));}close(fd);return 0;
}

此时log.txt文件中就写入了我们指定的内容:

image-20230822144214732

如果我们打开文件时选择追加,上面的操作则会不断向文件中追加相同的内容。


read

NAMEread - read from a file descriptorSYNOPSIS#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);DESCRIPTIONfd: 要读的文件描述符buf: 存放读取的数据count: 要读取的字节数RETURN VALUE如果成功,则返回读取的字节数如果出现错误,则返回-1,并适当地设置errno

刚刚我们创建一个文件并向其中写入,现在我们试着将写入的内容读出来并进行打印:

#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, 0666);if(fd < 0)          //打开文件失败{perror("open");return 1;}char buffer[1024];ssize_t num = read(fd, buffer, sizeof(buffer) - 1); //预留一个位置存放'\0'if(num > 0) buffer[num] = 0;printf("%s", buffer);close(fd);return 0;
}

image-20230822145048768

系统接口就简单介绍这么多。


再次谈论文件

文件描述符

一个进程就可以打开多个文件,而系统中又存在着这么多进程,所以系统中一定存在着大量的被打开文件,而且某些文件还可能被打开了多次,而这些被打开的文件毫无疑问也是要被操作系统所管理的。那问题来了,操作系统是怎么管理这些被打开文件的呢 —— 先描述,再组织

为了管理文件,必定要创建相应的内核数据结构来描述文件,这个内核数据结构就是struct file,结构体内部包含了文件的大部分属性,每一个文件都有一个对应的内核数据结构struct file,将这些结构通过一定的数据结构组织起来,通过算法对这部分数据结构进行增删查改,不就实现了对文件的管理吗?

前面介绍open的时候涉及到了文件描述符的概念,我们初步知道文件描述符是一个整数,那不妨试着打印一下这个整数:

#define FILE_NAME(number) "log.txt"#number int main()
{int fd[5] = { 0 };fd[0] = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666);fd[1] = open(FILE_NAME(2), O_WRONLY | O_CREAT | O_APPEND, 0666);fd[2] = open(FILE_NAME(3), O_WRONLY | O_CREAT | O_APPEND, 0666);fd[3] = open(FILE_NAME(4), O_WRONLY | O_CREAT | O_APPEND, 0666);fd[4] = open(FILE_NAME(5), O_WRONLY | O_CREAT | O_APPEND, 0666);for (int i = 0; i < 5; i++)printf("%d\n", fd[i]);return 0;
}

image-20230822150708204

此时能想到3、4、5、6、7像是数组下标,但为什么是从3开始呢?

这就涉及到前面提到的C默认打开的三个输入输出流:stdinstdoutstderr

我们用C语言打开文件时会返回一个FILE*,上面三个输入输出流其实也是FILE*类型的:

image-20230822151843315

FILE结构体里有一个字段_fileno,这个其实就是文件描述符,我们可以试着打印一下:

int main()
{printf("stdin -> %d\n", stdin->_fileno);printf("stdout -> %d\n", stdout->_fileno);printf("stderr -> %d\n", stderr->_fileno);return 0;
}

image-20230822152635701

带着现象和问题,下面进行讲解。

我们前面说了,C语言的一系列接口是封装的系统接口,而系统接口访问文件并不是依托文件名,而是文件描述符,那怎么通过文件描述符找到对应的文件呢?

系统为每个文件都创建了内核数据结构struct file,用以保存文件的大部分属性,而我们的进程需要找到文件,也就是进程需要通过一定的方式与许多的struct file结构关联起来,所以最好整一个指针数组,数组每一个元素都指向一个struct file,所以每个进程都有一个这样的指针数组struct file* fd_array[],称之为文件描述符表,所以进程只需要找到这张表就能找到要访问的文件。但是进程与文件的关系不光只靠文件描述符,还有其它的一些关系需要描述,而这些描述进程和文件之间关系的字段都被封装到了一个结构体struct files_struct中,进程的PCB中间接保存了指向这个结构体的指针struct files_struct *files,这样进程和它打开的所有文件就建立起了完整的连接。

下面用一张图来描述这个连接:

image-20230822160201224

综上,我们就知道了文件描述符就是从0开始的小整数,当我们第一次打开某个文件时,OS要在内存中给文件创建内核数据结构file来描述文件,表示一个已经打开的文件对象。

而进程打开文件时,必须让进程和被打开文件关联起来,进程的PCB中有一个指针files,指向一张表files_struct,该表中有一个字段是一个指针数组fd_array,每一个元素都是指向file的指针,而文件描述符就是该数组元素的下标。

因此,只要知道了下标,就可以找到对应的文件。


文件描述符的分配规则

先看下面这段代码:

int main()
{int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}

运行上面的代码,结果是fd: 3

因为每个进程默认会指向三个files,分别是标准输入,标准输出,标准错误(也是标准输出),所以下标0、1、2都被占用了,顺着就分配到了3。

那再看下面这段代码:

int main()
{close(0);int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}

此时代码的运行结果是fd: 0

由此我们可以推断出文件描述符的分配规则,即在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。


重定向

什么是重定向

有基础的小伙伴应该听说过输入重定向、输出重定向和追加重定向。

在命令行中我们可以通过<>>>分别实现输入重定向、输出重定向和追加重定向。

比如下面这样:

输出重定向:

image-20230827144201979

追加重定向:

image-20230827144233758

输入重定向:

image-20230827144330491

简单看来,重定向就是本该从file1输入或向file1输出,结果却从file2中输入或向file2中输出了。


重定向的本质

上面已经铺垫好了文件描述符的内容,所以现在打开一个进程会有下面的一个关系:

image-20230827144853486

我们还说C默认会打开三个文件分别是stdinstdoutstderr,而这三个东西的本质是指向三个FILE结构的指针,在每个FILE结构里分别存放了一个文件描述符,依次是0、1、2,所以stdin默认和0绑定,stdout默认和1绑定,stderr默认和2绑定。

所以我们像stdout中写入本质是像文件描述符1映射的文件写入,而1默认是和显示器建立映射关系的,如果我们手动改变这个映射关系呢?

以下面的代码为例:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>int main()
{close(1);int fd = open("myfile", O_WRONLY|O_CREAT, 00644);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);fflush(stdout);close(fd);exit(0);
}

首先调用close接口关掉了1,也就是断掉了文件描述符1和显示器之间的映射关系:

image-20230827145340895

然后我们又打开了一个文件myfile,按照文件描述符的分配规则,1现在会和刚打开的文件建立起新的映射关系:

image-20230827145551659

此时我们再调用printfstdout中输出会发生什么呢?

image-20230827150048369

此时并没有向显示器输出,而是输出到了新打开的文件。

再理解一下上面那个过程:

首先通过调用close接口断开1与显示器的映射关系,注意stdout并不是直接关联的显示器,而是它指向的FILE结构体对象内部存储的文件描述符是1,fd_array[1]默认指向OS给显示器创建的内核数据结构。

此时我们再打开一个文件,然后根据文件描述符的分配规则,OS发现1是空的,于是1就指向了新打开的文件,当我们用printf打印时,由于上层stdout的文件描述符还是存的1,就会在内核中寻找fd_array[1]对应的文件进行打印操作,而此时1已经不再映射显示器,而是myfile,随意此时打印的内容就到了myfile中。

所以重定向的本质是上层用到的文件描述符不变,在内核中更改文件描述符映射的文件


系统调用接口实现重定向

我们可以通过上面的方法先closeopen实现重定向,但不够优雅。

实际上操作系统也提供了实现重定向的接口dup/dup2/dup3,下面只介绍dup2

SYNOPSIS#include <unistd.h>int dup2(int oldfd, int newfd);DESCRIPTIONdup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:*  If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.*  If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.RETURN VALUEOn success, dup2 returns the new descriptor. On error, -1 is returned, and errno is set appropriately.

以上内容来自man手册。

解释一下,dup2会拷贝oldfdnewfd中,如果必要的时候会先关掉newfd,但是还有两点需要注意的:

  1. 如果oldfd是无效的文件描述符,那么调用就会失败,原有的newfd也不会关闭。
  2. 如果oldfd是有效的文件描述符,而newfdoldfd一样,也就是传进来的两个文件描述符是相同的,那么函数什么也不干。

此时有这样的映射关系:

image-20230827233134029

然后我们试着调用dup2(4, 1),映射关系就会发生下面的变化:

image-20230828001912871

此时再向stdout中输出,也就完成了输出重定向。


<、>、>>

命令行解释器中我们可以通过<>>>分别实现输入重定向。输出重定向、追加重定向,那这是怎么实现的呢?

我们可以写一个简易的命令行解释器demo:

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <string.h>char  lineCommand[1024];  // 接收输入的命令
char* myargv[64];         // 存储解析的命令行参数
int   lastCode;           // 保存子进程的退出码
int   lastSig;            // 保存子进程的退出信号
char  pwd[64];            // 保存当前所在文件路径//解析当前位于哪个文件夹下
char* get_path(char* pwd)
{int pre = 0, cur = 0;while (pwd[cur]){cur++;if (pwd[cur] == '/'){pre = ++cur;}}return pwd + pre;
}int main()
{ // 初始化pwdstrcpy(pwd, getenv("PWD"));while (1){// 打印命令行提示符,并将其从缓冲区刷新打印printf("[%s@%s %s]$ ", getenv("LOGNAME"), getenv("HOSTNAME"), get_path(pwd));fflush(stdout);   // 立即刷新stdout的缓冲区// 用户输入命令,记得去掉最后一个\nchar* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);   // 预留一个位置存'\0'assert(s != NULL);lineCommand[strlen(lineCommand) - 1] = 0;  // 去掉最后一个'\n'(void)s;   // 将s置空,意思就是后面不用s了// 将命令分解为一个个单字符串int i = 0;myargv[i++] = strtok(lineCommand, " ");while (myargv[i++] = strtok(NULL, " "));// 如果命令是ls,则可能需要配置颜色if (myargv[0] != NULL && strcmp("ls", myargv[0]) == 0){myargv[i - 1] = (char*)"--color=auto";myargv[i] = NULL;}// 如果命令是ll,则需要特殊处理一下if (myargv[0] != NULL && strcmp("ll", myargv[0]) == 0){myargv[0][1] = 's';myargv[i - 1] = (char*)"--color=auto";myargv[i] = (char*)"-l";myargv[i + 1] = NULL;}// 如果命令是echo,则在当前进程就可以完成,为内建命令// 这里只支持输出上个进程的退出信息和普通信息if (myargv[0] != NULL && myargv[1] != NULL && strcmp("echo", myargv[0]) == 0){// 输出上一个进程的退出信息if (strcmp("$?", myargv[1]) == 0)printf("code:%d\tsig:%d\n", lastCode, lastSig);// 有啥输出啥else printf("%s\n", myargv[1]);continue;}// 如果命令是cd,则只能在当前进程完成,因为需要修改PWD - 当前路径if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0){if(myargv[1] != NULL){chdir(myargv[1]);strcpy(pwd, myargv[1]);  // 当前所在文件路径也要随之改变}continue;}// 创建子进程进行进程替换pid_t id = fork();assert(id != -1);if (id == 0){execvp(myargv[0], myargv);exit(1);}int status = 0;pid_t ret = waitpid(id, &status, 0);assert(ret > 0);(void)ret;lastCode = (status >> 8) & 0xFF;lastSig = status & 0x7F;}return 0;
}

在此基础上我们可以添加重定向的功能。

首先我们定义几个宏表示重定向类型,并定义保存重定向类型和重定向文件的变量:

#define NONE_REDIR   0  // 无重定向
#define INPUT_REDIR  1  // 输入重定向
#define OUTPUT_REDIR 2  // 输出重定向
#define APPEND_REDIR 3  // 追加重定向int redirType = NONE_REDIR;  // 记录重定向类型
char *redirFile = NULL;		 // 记录重定向文件

首先我们需要解析命令,判断当前是什么重定向类型并记录重定向的文件,所以我们可以写一个command_check函数完成这部分内容:

void command_check(char *commands)
{assert(commands);char *start = commands;char *end = commands + strlen(commands);// 开始遍历命令while(start < end){// 遍历到了>,此时可能是输出重定向,也可能是追加重定向if(*start == '>'){// 首先将这个位置置零,之后解析命令行参数就用不到之后的内容了*start = '\0';start++;// 继续判断下一个字符,如果是追加重定向if(*start == '>'){// 类似这样的形式"ls -a >> file.log"// 首先设置重定向类型// 但是不要着急保存文件信息// 因为可能存在这种情况"ls -a >>   file.log"redirType = APPEND_REDIR;start++;}// 否则就是输出重定向else{redirType = OUTPUT_REDIR;}// 我们可以写一个trim_space函数或宏帮我们跳过空格// 之后才可保存文件信息trim_space(start);redirFile = start;break;}// 遍历到了<,此时就是输入重定向else if(*start == '<'){*start = '\0';start++;redirType = INPUT_REDIR;// 确保去掉先导空格才可记录重定向文件trim_space(start);redirFile = start;break;}else{start++;}}
}

补充一下上面出现的trim_space函数,这里写成了宏函数的形式:

#define trimSpace(start) do{\while(isspace(*start)) ++start;\}while(0)

因为真正的命令是靠子进程来执行的,所以重定向工作需要子进程完成,并且在进程替换之前就要做好所有的重定向工作。在执行进程替换之前,需要根据父进程提供的信息判断一下是否需要重定向,如果需要的话则执行相关命令:

switch(redirType)
{case NONE_REDIR:break;case INPUT_REDIR:{// 先以只读的方式打开文件int fd = open(redirFile, O_RDONLY);// 如果打开文件失败就退出进程if(fd < 0){perror("open");exit(errno);}// 输入重定向,改变文件描述符0的指向dup2(fd, 0);}break;case OUTPUT_REDIR:case APPEND_REDIR:{umask(0);// 无论是输入重定向还是输出重定向,都需要以写的方式打开文件// 并且文件不存在时需要创建文件int flags = O_WRONLY | O_CREAT;// 如果是追加重定向,则添加要进行追加的信息if(redirType == APPEND_REDIR) flags |= O_APPEND;// 如果是输出重定向,则需要先清空文件else flags |= O_TRUNC;// 确定好打开文件的方式之后打开文件int fd = open(redirFile, flags, 0666);if(fd < 0){perror("open");exit(errno);}// 输出和追加都是像显示器输出或追加,所以需要改变文件描述符1的指向dup2(fd, 1);}break;
}

在每次开始时都初始化一下重定向信息和错误信息,添加相关头文件,就得到了完整代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <assert.h>
#include <errno.h>#define NONE_REDIR   0  // 无重定向
#define INPUT_REDIR  1  // 输入重定向
#define OUTPUT_REDIR 2  // 输出重定向
#define APPEND_REDIR 3  // 追加重定向#define trim_space(start) do{\while(isspace(*start)) ++start;\}while(0)int redirType = NONE_REDIR;  // 记录重定向类型
char *redirFile = NULL;		 // 记录重定向文件char  lineCommand[1024];  // 接收输入的命令
char* myargv[64];         // 存储解析的命令行参数
int   lastCode;           // 保存子进程的退出码
int   lastSig;            // 保存子进程的退出信号
char  pwd[1024];          // 保存当前所在文件夹名//解析当前位于哪个路径下
char* get_path(char* pwd)
{int pre = 0, cur = 0;while (pwd[cur]){cur++;if (pwd[cur] == '/'){pre = ++cur;}}return pwd + pre;
}void command_check(char *commands)
{assert(commands);char *start = commands;char *end = commands + strlen(commands);// 开始遍历命令while(start < end){// 遍历到了>,此时可能是输出重定向,也可能是追加重定向if(*start == '>'){// 首先将这个位置置零,之后解析命令行参数就用不到之后的内容了*start = '\0';start++;// 继续判断下一个字符,如果是追加重定向if(*start == '>'){// 类似这样的形式"ls -a >> file.log"// 首先设置重定向类型// 但是不要着急保存文件信息// 因为可能存在这种情况"ls -a >>   file.log"redirType = APPEND_REDIR;start++;}// 否则就是输出重定向else{redirType = OUTPUT_REDIR;}// 我们可以写一个trim_space函数或宏帮我们跳过空格// 之后才可保存文件信息trim_space(start);redirFile = start;break;}// 遍历到了<,此时就是输入重定向else if(*start == '<'){*start = '\0';start++;redirType = INPUT_REDIR;// 确保去掉先导空格才可记录重定向文件trim_space(start);redirFile = start;break;}else{start++;}}
}int main()
{ // 解析当前文件夹名strcpy(pwd, getenv("PWD"));while (1){// 初始化重定向信息和错误信息redirType = NONE_REDIR;redirFile = NULL;errno = 0;// 打印命令行提示符,并将其从缓冲区刷新打印printf("[%s@%s %s]$ ", getenv("LOGNAME"), getenv("HOSTNAME"), get_path(pwd));fflush(stdout);   // 立即刷新stdout的缓冲区// 用户输入命令,记得去掉最后一个\nchar* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);   // 预留一个位置存'\0'assert(s != NULL);lineCommand[strlen(lineCommand) - 1] = 0;  // 去掉最后一个'\n'(void)s;// 将命令分解为一个个单字符串int i = 0;myargv[i++] = strtok(lineCommand, " ");while (myargv[i++] = strtok(NULL, " "));// 如果命令是ls,则可能需要配置颜色if (myargv[0] != NULL && strcmp("ls", myargv[0]) == 0){myargv[i - 1] = (char*)"--color=auto";myargv[i] = NULL;}// 如果命令是ll,则需要特殊处理一下if (myargv[0] != NULL && strcmp("ll", myargv[0]) == 0){myargv[0][1] = 's';myargv[i - 1] = (char*)"--color=auto";myargv[i] = (char*)"-l";myargv[i + 1] = NULL;}// 如果命令是echo,则在当前进程就可以完成,为内建命令if (myargv[0] != NULL && myargv[1] != NULL && strcmp("echo", myargv[0]) == 0){// 输出上一个进程的退出信息if (strcmp("$?", myargv[1]) == 0)printf("code:%d\tsig:%d\n", lastCode, lastSig);else printf("%s\n", myargv[1]);continue;}// 如果命令是cd,则只能在当前进程完成,因为需要修改PWD - 当前路径if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0){if(myargv[1] != NULL){chdir(myargv[1]);strcpy(pwd, myargv[1]);get_path(pwd);}continue;}// 创建子进程进行进程替换pid_t id = fork();assert(id != -1);if (id == 0){switch(redirType){case NONE_REDIR:break;case INPUT_REDIR:{// 先以只读的方式打开文件int fd = open(redirFile, O_RDONLY);if(fd < 0){perror("open");exit(errno);}// 输入重定向,改变文件描述符0的指向dup2(fd, 0);}break;case OUTPUT_REDIR:case APPEND_REDIR:{umask(0);// 无论是输入重定向还是输出重定向,都需要以写的方式打开文件// 并且文件不存在时需要创建文件int flags = O_WRONLY | O_CREAT;// 如果是追加重定向,则添加要进行追加的信息if(redirType == APPEND_REDIR) flags |= O_APPEND;// 如果是输出重定向,则需要先清空文件else flags |= O_TRUNC;// 确定好打开文件的方式之后打开文件int fd = open(redirFile, flags, 0666);if(fd < 0){perror("open");exit(errno);}// 输出和追加都是像显示器输出或追加,所以需要改变文件描述符1的指向dup2(fd, 1);}break;}execvp(myargv[0], myargv);exit(1);}int status = 0;pid_t ret = waitpid(id, &status, 0);assert(ret > 0);(void)ret;lastCode = (status >> 8) & 0xFF;lastSig = status & 0x7F;}return 0;
}

这里需要想明白一件事,重定向并不会影响父进程,因为进程之间具有独立性。

相关文章:

【Linux】深入理解文件操作

文章目录 初次谈论文件重温C语言文件操作系统文件操作接口openwriteread 再次谈论文件文件描述符文件描述符的分配规则 重定向什么是重定向重定向的本质系统调用接口实现重定向<、>、>> 初次谈论文件 开始之前先谈论一下关于文件的一些共识性问题。 一个文件可以…...

异地使用PLSQL远程连接访问Oracle数据库【内网穿透】

文章目录 前言1. 数据库搭建2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射 3. 公网远程访问4. 配置固定TCP端口地址4.1 保留一个固定的公网TCP端口地址4.2 配置固定公网TCP端口地址4.3 测试使用固定TCP端口地址远程Oracle 前言 Oracle&#xff0c;是甲骨文公司的一款关系…...

【方案】基于AI边缘计算的智慧工地解决方案

一、方案背景 在工程项目管理中&#xff0c;工程施工现场涉及面广&#xff0c;多种元素交叉&#xff0c;状况较为复杂&#xff0c;如人员出入、机械运行、物料运输等。特别是传统的现场管理模式依赖于管理人员的现场巡查。当发现安全风险时&#xff0c;需要提前报告&#xff0…...

华为各型号交换机开启SNMP v3

设备型号&#xff1a;华为S5720S-28P-LI-AC 设备软件版本&#xff1a;V200R011C10SPC600 调试命令&#xff1a; snmp-agent snmp-agent sys-info version v3 snmp-agent group v3 GroupName privacy //{GroupName}是设置一个SNMP的组名&#xff0c;我设置是SNMPGroup snm…...

CocosCreator3.8研究笔记(一)windows环境安装配置

一、安装Cocos 编辑器 &#xff08;1&#xff09;、下载Cocos Dashboard安装文件 Cocos 官方网站Cocos Dashboard下载地址 &#xff1a; https://www.cocos.com/creator-download9下载完成后会得到CocosDashboard-v2.0.1-win-082215.exe 安装文件&#xff0c;双击安装即可。 …...

【JavaWeb 专题】15个最经典的JavaWeb面试题

文章目录 HTTP长连接和短连接HTTP/1.1 与 HTTP/1.0 的区别可扩展性缓存带宽优化长连接消息传递Host 头域错误提示 AjaxAjax 的优势&#xff1a; JSP 和 servlet 有什么区别&#xff1f;定义区别 JSP 的9大内置对象及作用JSP 的 4 种作用域&#xff1f;session 和 cookie 有什么…...

力扣:75. 颜色分类(Python3)

题目&#xff1a; 给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums &#xff0c;原地对它们进行排序&#xff0c;使得相同颜色的元素相邻&#xff0c;并按照红色、白色、蓝色顺序排列。 我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。 必须在不使用库内置的 sort …...

JVM 内存大对象监控和优化实践

作者&#xff1a;vivo 互联网服务器团队 - Liu Zhen、Ye Wenhao 服务器内存问题是影响应用程序性能和稳定性的重要因素之一&#xff0c;需要及时排查和优化。本文介绍了某核心服务内存问题排查与解决过程。首先在JVM与大对象优化上进行了有效的实践&#xff0c;其次在故障转移与…...

vue indexedDB 取指定数据库指定表 全部key用request.onsuccess

1 例子 export async function funcGetKey(dbName, tableName) {return new Promise((resolve, reject) > {// 打开指定的数据库const request indexedDB.open(dbName);request.onerror (event) > {console.error(打开数据库失败: , event.target.error);reject(event…...

Java 数据结构使用学习

Set和List的区别 Set 接口实例存储的是无序的&#xff0c;不重复的数据。List 接口实例存储的是有序的&#xff0c;可以重复的元素。 Set 检索效率低下&#xff0c;删除和插入效率高&#xff0c;插入和删除不会引起元素位置改变 <实现类有HashSet,TreeSet>。 List 和数…...

monorepo更新组件报错,提示“无法加载文件 C:\Program Files\nodejs\pnpm.ps1,因为在此系统上禁止运行脚本”

解决方法&#xff1a; 第一步&#xff1a;管理员身份运行 window.powershell&#xff0c; win x打开powerShell命令框&#xff0c;进入到对应项目路径。 第二步&#xff1a;执行&#xff1a;get-ExecutionPolicy&#xff0c;显示Restricted&#xff0c;表示状态是禁止的; 第…...

vue中html引入使用<%= BASE_URL %>变量

首先使用src相对路径引入 注意&#xff1a; js 文件放在public文件下 不要放在assets静态资源文件下 否则 可能会报错 GET http://192.168.0.113:8080/src/assets/js/websockets.js net::ERR_ABORTED 500 (Internal Server Error) 正确使用如下&#xff1a;eg // html中引…...

Android全面屏下,默认不会全屏显示,屏幕底部会留黑问题

前些天发现了一个蛮有意思的人工智能学习网站,8个字形容一下"通俗易懂&#xff0c;风趣幽默"&#xff0c;感觉非常有意思,忍不住分享一下给大家。 &#x1f449;点击跳转到教程 公司以前的老项目&#xff0c;便出现了这种情况&#xff0c;网上搜索了各种资料&#xf…...

5.Redis-string

string 字符串 字符串类型是 Redis 最基础的数据类型&#xff0c;关于字符串需要特别注意&#xff1a; 1.⾸先Redis中所有 key 的类型都是字符串类型&#xff0c;⽽且其他⼏种数据结构也都是在字符串类似基础上构建的&#xff0c;例如 list 和 set 的元素类型是字符串类型。 2…...

docker高级(redis集群三主三从)

1. 新建6个docker容器redis实例 docker run -d --name redis-node-1 --net host --privilegedtrue -v /redis/share/redis-node-1:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6381docker run -d --name redis-node-2 --net host --privilegedtrue -v /…...

linux 设置与命令基础(二)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、系统基本操作 二、命令类型 三、命令语法 四、命令补齐 五、命令帮助 六、系统基本操作命令 总结 前言 这是本人学习Linux的第二天&#xff0c;今天主…...

ubuntu20.04中ros2安装rosbridge及启动方式

ros2 启动rosbridge&#xff1a; 要启动ROS2中的rosbridge&#xff0c;需要先安装ROS2的rosbridge_suite软件包。使用以下命令安装&#xff1a; sudo apt-get update sudo apt-get install ros-<distro>-rosbridge-suite将<distro>替换为正在使用的ROS2发行版的名…...

TCP之超时重传、流量控制和拥塞控制

一、超时重传 TCP超时重传是TCP协议中的一种机制&#xff0c;用于在发生丢包或数据包未及时确认的情况下&#xff0c;重新发送未确认的数据段。 当发送方发送一个数据段后&#xff0c;会启动一个定时器&#xff08;称为超时计时器&#xff09;&#xff0c;等待接收方的确认。…...

git clone 报SSL证书问题

git命令下运行 git config --global http.sslVerify false 然后再进行重新clone代码...

Spring Boot 排除配置类的引用的方法

Spring Boot 提供的自动配置非常强大&#xff0c;某些情况下&#xff0c;自动配置的功能可能不符合我们的需求&#xff0c;需要我们自定义配置&#xff0c;这个时候就需要排除/禁用 Spring Boot 某些类的自动化配置了。 比如&#xff1a;数据源、邮件&#xff0c;这些都是提供…...

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…...

rknn优化教程(二)

文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK&#xff0c;开始写第二篇的内容了。这篇博客主要能写一下&#xff1a; 如何给一些三方库按照xmake方式进行封装&#xff0c;供调用如何按…...

微信小程序云开发平台MySQL的连接方式

注&#xff1a;微信小程序云开发平台指的是腾讯云开发 先给结论&#xff1a;微信小程序云开发平台的MySQL&#xff0c;无法通过获取数据库连接信息的方式进行连接&#xff0c;连接只能通过云开发的SDK连接&#xff0c;具体要参考官方文档&#xff1a; 为什么&#xff1f; 因为…...

浅谈不同二分算法的查找情况

二分算法原理比较简单&#xff0c;但是实际的算法模板却有很多&#xff0c;这一切都源于二分查找问题中的复杂情况和二分算法的边界处理&#xff0c;以下是博主对一些二分算法查找的情况分析。 需要说明的是&#xff0c;以下二分算法都是基于有序序列为升序有序的情况&#xf…...

在WSL2的Ubuntu镜像中安装Docker

Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包&#xff1a; for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...

ArcGIS Pro制作水平横向图例+多级标注

今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作&#xff1a;ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等&#xff08;ArcGIS出图图例8大技巧&#xff09;&#xff0c;那这次我们看看ArcGIS Pro如何更加快捷的操作。…...

RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)

RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发&#xff0c;后来由Pivotal Software Inc.&#xff08;现为VMware子公司&#xff09;接管。RabbitMQ 是一个开源的消息代理和队列服务器&#xff0c;用 Erlang 语言编写。广泛应用于各种分布…...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

淘宝扭蛋机小程序系统开发:打造互动性强的购物平台

淘宝扭蛋机小程序系统的开发&#xff0c;旨在打造一个互动性强的购物平台&#xff0c;让用户在购物的同时&#xff0c;能够享受到更多的乐趣和惊喜。 淘宝扭蛋机小程序系统拥有丰富的互动功能。用户可以通过虚拟摇杆操作扭蛋机&#xff0c;实现旋转、抽拉等动作&#xff0c;增…...

【Linux手册】探秘系统世界:从用户交互到硬件底层的全链路工作之旅

目录 前言 操作系统与驱动程序 是什么&#xff0c;为什么 怎么做 system call 用户操作接口 总结 前言 日常生活中&#xff0c;我们在使用电子设备时&#xff0c;我们所输入执行的每一条指令最终大多都会作用到硬件上&#xff0c;比如下载一款软件最终会下载到硬盘上&am…...