【Linux】深入理解文件操作

news2025/4/20 2:04:14

文章目录

      • 初次谈论文件
      • 重温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

NAME
	write - write to a file descriptor

SYNOPSIS
	#include <unistd.h>
	ssize_t write(int fd, const void *buf, size_t count);

DESCRIPTION
    fd: 要写入的文件描述符
    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

NAME
	read - read from a file descriptor

SYNOPSIS
	#include <unistd.h>
	ssize_t read(int fd, void *buf, size_t count);

DESCRIPTION
	fd: 要读的文件描述符
	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);

DESCRIPTION
    dup2() 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 VALUE
	On 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()
{ 
    // 初始化pwd
    strcpy(pwd, getenv("PWD"));

    while (1)
    {
        // 打印命令行提示符,并将其从缓冲区刷新打印
        printf("[%s@%s %s]$ ", getenv("LOGNAME"), getenv("HOSTNAME"), get_path(pwd));
        fflush(stdout);   // 立即刷新stdout的缓冲区
        
        // 用户输入命令,记得去掉最后一个\n
        char* 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的缓冲区
        
        // 用户输入命令,记得去掉最后一个\n
        char* 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;
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/937527.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(笔记一)利用open_cv在图像上进行点标记,文字注记,画圆、多边形、椭圆

&#xff08;1&#xff09;CV2中的绘图函数&#xff1a; cv2.line() 绘制线条cv2.circle() 绘制圆cv2.rectangle() 绘制矩形cv2.ellipse() 绘制椭圆cv2.putText() 添加注记 &#xff08;2&#xff09;注释 img表示需要绘制的图像color表示线条的颜色&#xff0c;采用颜色矩阵…

联想电脑装系统无法按F9后无法从系统盘启动的解决方案

开机时按F9发现没有加载系统盘. 打开BIOS设置界面&#xff0c;调整设置如下: BOOT MODE: Legacy Support.允许legacy方式boot. BOOT PRIORITY: Legacy First. Legacy方式作为首选的boot方式. USB BOOT: ENABLED. 允许以usb方式boot. Legacy: 这里设置legacy boot的优先级,…

保姆级教程:从0到1使用Stable Diffusion XL训练LoRA模型 |【人人都是算法专家】

Rocky Ding 公众号&#xff1a;WeThinkIn 写在前面 【人人都是算法专家】栏目专注于分享Rocky在AI行业中对业务/竞赛/研究/产品维度的思考与感悟。欢迎大家一起交流学习&#x1f4aa; 大家好&#xff0c;我是Rocky。 Rocky在知乎上持续撰写Stable Diffusion XL全方位的解析文章…

HTML之VSCode简单配置与创建

目录 插件下载 然后输入源码&#xff1a; 使用 效果 插件下载 下载这个插件后可以直接运行&#xff1a; 然后创建一个文件&#xff1a; 然后输入源码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"…

渗透测试工具ZAP入门教程(1)-安装和快速开始

介绍 ZAP Zed Attack Proxy&#xff08;ZAP&#xff09;是一个免费的开源渗透测试工具&#xff0c;在 软件安全项目 &#xff08;SSP&#xff09;。ZAP 专为测试 Web 应用程序而设计&#xff0c;既灵活又可扩展。 ZAP的核心是所谓的“中间人代理”。它位于测试人员的浏览器和…

【算法训练-双指针】最长无重复子串(数组)

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;本篇Blog的主题是最长无重复子串或最长无重复子数组&#xff0c;这类题目出现频率还是很高的。 最长无重复子数组 先来看看数组数据结构的题目 题干 输入&#…

Shiro认证框架

目录 概述 认证授权及鉴权 Shiro框架的核心组件 基本流程 spring bootshiromybatisPlus...实现用户登录 step1:准备工作 (1)坐标 (2)连接数据库 (3)JavaBean (4)dao数据访问层 (5)密码工具类 DigestsUtil (6)配置类 step2&#xff1a;认证功能 step3:授权鉴权 概述…

11. 网络模型保存与读取

11.1 网络模型保存(方式一) import torchvision import torch vgg16 torchvision.models.vgg16(pretrainedFalse) torch.save(vgg16,"./model/vgg16_method1.pth") # 保存方式一&#xff1a;模型结构 模型参数 print(vgg16) 结果&#xff1a; VGG((feature…

飞腾FT-2000/4、D2000 log报错指导(2)

在爱好者群中遇见了很多的固件问题,这里总结记录了大家的交流内容和调试心得。主要是飞腾桌面CPU FT-2000/4 D2000相关的,包含uboot和UEFI。希望对大家调试有所帮助。 这个专题会持续更新,凑够一些就发。 12 UEFI现象:主板启动时串口信息停在 s3 flag form ec 处,如下图所…

局域网中电脑共享文件给手机

学习资源&#xff1a; 局域网共享&#xff1a;这样设置&#xff0c;你可以轻松拷贝任何电脑的文件。_哔哩哔哩_bilibili 可以实现什么效果&#xff1f; 连接同一个WIFI&#xff0c;电脑端为服务端&#xff0c;提供共享文件&#xff0c;手机是客户端&#xff0c;可以读取服务端…

Java-泛型

文章目录 Java泛型什么是泛型&#xff1f;在哪里使用泛型&#xff1f;设计出泛型的好处是什么&#xff1f;动手设计一个泛型泛型的限定符泛型擦除泛型的通配符 结论 Java泛型 什么是泛型&#xff1f; Java泛型是一种编程技术&#xff0c;它允许在编译期间指定使用的数据类型。…

Leetcode:238. 除自身以外数组的乘积【题解超详细】

纯C语言实现&#xff08;小白也能看明白&#xff09; 题目 给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数…

8. 损失函数与反向传播

8.1 损失函数 ① Loss损失函数一方面计算实际输出和目标之间的差距。 ② Loss损失函数另一方面为我们更新输出提供一定的依据。 8.2 L1loss损失函数 ① L1loss数学公式如下图所示&#xff0c;例子如下下图所示。 import torch from torch.nn import L1Loss inputs torch.tens…

冷启问题目前常见解决方案

1、冷启动的基本方式 随机冷启 个性化冷启 2、冷启动解决问题 冷启动保量 冷启动保量&#xff0c;保证每个item曝光的次数一样&#xff0c;实时统计已曝光的次数和要曝光的次数 冷启动结束过滤&#xff0c;如果需曝光的量越大&#xff0c;且越接近冷启结束时间&#xff0…

3.2 数据的表现形式及其运算

3.2.1 常量和变量 在计算机高级语言中&#xff0c;数据有两种表现形式&#xff1a;常量和变量。 1.常量 在程序运行过程中&#xff0c;其值不能被改变的量称为常量。如例3.1程序中的5,9,32和例3.2程序中的1000,0.0036,0.0225,0.0198是常量。数值常量就是数学中的常数。 常用的…

三、前端监控之Sentry的介绍

Sentry介绍 Sentry是一个开源的实时事件日志记录和聚合平台。它专门用于监视错误和提取执行适当的事后操作所需的所有信息, 而无需使用标准用户反馈循环的任何麻烦。支持 web 前后端、移动应用以及游戏&#xff0c;支持 Python、OC、Java、Go、Node、Django、RoR 等主流编程语…

【Docker】网络

文章目录 Docker 网络基础Docker网络管理Docker网络架构CNMLibnetwork驱动 常见的网络类型 Docker 网络管理命令docker network createdocker network inspectdocker network connectdocker network disconnectdocker network prunedocker network rmdocker network ls docker …

跳跃游戏 II【贪心算法】

跳跃游戏 II class Solution {public int jump(int[] nums) {int cur 0;//当前最大覆盖路径int next 0;//下一步的最大覆盖路径int res 0;//存放结果&#xff0c;到达终点时最少的跳跃步数for (int i 0; i < nums.length; i) {//遍历数组&#xff0c;以给出数组以一个…

老韦上新

知道韦东山老师的人很多&#xff0c;当然肯定可能也是有人不知道的&#xff0c;比如我今天吃饭的时候&#xff0c;我跟 JP 同学说&#xff0c;你认识韦东山老师吗&#xff1f;JP 同学说&#xff0c;「我不认识&#xff0c;他是不是搞 ODM 的&#xff1f;你知道的&#xff0c;我…

【python】输出高亮信息的内容

背景 日志是定位问题和数据分析的关键手段之一&#xff0c;尤其是在调试阶段&#xff0c;高效的、具有辨识度的日志可以非常快速准确的进行问题定位。shell中的echo命令自带文本格式化输出的功能&#xff0c;我们先来回顾下基本的语法&#xff0c;然后套用到python中即可。 s…