【Linux】深入理解文件操作
文章目录
- 初次谈论文件
- 重温C语言文件操作
- 系统文件操作接口
- open
- write
- read
- 再次谈论文件
- 文件描述符
- 文件描述符的分配规则
- 重定向
- 什么是重定向
- 重定向的本质
- 系统调用接口实现重定向
- <、>、>>
初次谈论文件
开始之前先谈论一下关于文件的一些共识性问题。
- 一个文件可以分为两部分,内容和属性。
- 基于上面的认识,空文件也要在磁盘中占据空间,因为空文件的内容为空,但是还有属性在,例如文件的创建时间… 而这部分属性也是要存储的。
- 所以对文件的操作就变成了对文件的内容或对文件的属性进行操作。
- 如果我们要标识一个文件,必须要通过
路径+文件名
的方式来唯一标识。 - 当我们使用相关方法进行文件操作的时候我们一般只写一个文件名,此时我们并没有指明文件的路径,此时默认是在
当前路径(访问文件的进程的当前路径)
下进行相关文件操作。 - 当我们写完一份代码,代码中有对文件进行操作的内容,当我们把代码编译成可执行文件后,在我们执行这个程序之前,文件操作并没有被执行,只有当我们运行这个程序,程序变成进程之后才会真正执行相应的文件操作,所以
对文件操作本质上是进程对文件的操作
。 - 我们无论是以哪种方式访问文件,前提是都要打开这个文件。而打开文件这个动作是谁完成的呢?谁能管理文件的存储谁就能打开,所以是操作系统打开的,或者准确一点,是操作系统收到进程的访问请求时打开的。所以
文件操作的本质是进程和被打开文件之间的关系
。
重温C语言文件操作
我们常说C默认会打开三个输入输出流:stdin
、stdout
、stderr
,这点后面还会用到。
C语言中有诸如fopen
、fclose
、fprintf
、fscanf
、fwrite
、fread
等涉及文件操作的函数方法。
这里就简单回顾一下部分接口。
-
FILE * fopen ( const char * filename, const char * mode );
用
fopen
函数可以打开一个文件,参数分别是文件名和打开方式,返回一个FILE
指针。而
mode
有多种选项,例如r - 只读
,r+ - 可读可写
,w - 只写
,w+ - 可读可写
,a - 追加
,a+ - 可追加可读
…除此之外还有二进制读写、文件不存在是否创建文件、文件是否会覆盖重写等细节问题。
-
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 << 0
,O_CREAT
可能是1 << 1
,O_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;
}
因为是以只写的方式打开,文件不存在我们选择创建文件,所以可以成功创建文件,而文件的权限并不是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
文件中就写入了我们指定的内容:
如果我们打开文件时选择追加,上面的操作则会不断向文件中追加相同的内容。
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;
}
系统接口就简单介绍这么多。
再次谈论文件
文件描述符
一个进程就可以打开多个文件,而系统中又存在着这么多进程,所以系统中一定存在着大量的被打开文件,而且某些文件还可能被打开了多次,而这些被打开的文件毫无疑问也是要被操作系统所管理的。那问题来了,操作系统是怎么管理这些被打开文件的呢 —— 先描述,再组织。
为了管理文件,必定要创建相应的内核数据结构来描述文件,这个内核数据结构就是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;
}
此时能想到3、4、5、6、7像是数组下标,但为什么是从3开始呢?
这就涉及到前面提到的C默认打开的三个输入输出流:stdin
,stdout
,stderr
。
我们用C语言打开文件时会返回一个FILE*
,上面三个输入输出流其实也是FILE*
类型的:
FILE
结构体里有一个字段_fileno
,这个其实就是文件描述符,我们可以试着打印一下:
int main()
{printf("stdin -> %d\n", stdin->_fileno);printf("stdout -> %d\n", stdout->_fileno);printf("stderr -> %d\n", stderr->_fileno);return 0;
}
带着现象和问题,下面进行讲解。
我们前面说了,C语言的一系列接口是封装的系统接口,而系统接口访问文件并不是依托文件名,而是文件描述符,那怎么通过文件描述符找到对应的文件呢?
系统为每个文件都创建了内核数据结构struct file
,用以保存文件的大部分属性,而我们的进程需要找到文件,也就是进程需要通过一定的方式与许多的struct file
结构关联起来,所以最好整一个指针数组,数组每一个元素都指向一个struct file
,所以每个进程都有一个这样的指针数组struct file* fd_array[]
,称之为文件描述符表,所以进程只需要找到这张表就能找到要访问的文件。但是进程与文件的关系不光只靠文件描述符,还有其它的一些关系需要描述,而这些描述进程和文件之间关系的字段都被封装到了一个结构体struct files_struct
中,进程的PCB中间接保存了指向这个结构体的指针struct files_struct *files
,这样进程和它打开的所有文件就建立起了完整的连接。
下面用一张图来描述这个连接:
综上,我们就知道了文件描述符就是从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
数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
重定向
什么是重定向
有基础的小伙伴应该听说过输入重定向、输出重定向和追加重定向。
在命令行中我们可以通过<
、>
、>>
分别实现输入重定向、输出重定向和追加重定向。
比如下面这样:
输出重定向:
追加重定向:
输入重定向:
简单看来,重定向就是本该从file1
输入或向file1
输出,结果却从file2
中输入或向file2
中输出了。
重定向的本质
上面已经铺垫好了文件描述符的内容,所以现在打开一个进程会有下面的一个关系:
我们还说C默认会打开三个文件分别是stdin
、stdout
和stderr
,而这三个东西的本质是指向三个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和显示器之间的映射关系:
然后我们又打开了一个文件myfile
,按照文件描述符的分配规则,1现在会和刚打开的文件建立起新的映射关系:
此时我们再调用printf
向stdout
中输出会发生什么呢?
此时并没有向显示器输出,而是输出到了新打开的文件。
再理解一下上面那个过程:
首先通过调用close
接口断开1与显示器的映射关系,注意stdout
并不是直接关联的显示器,而是它指向的FILE
结构体对象内部存储的文件描述符是1,fd_array[1]
默认指向OS给显示器创建的内核数据结构。
此时我们再打开一个文件,然后根据文件描述符的分配规则,OS发现1是空的,于是1就指向了新打开的文件,当我们用printf
打印时,由于上层stdout
的文件描述符还是存的1,就会在内核中寻找fd_array[1]
对应的文件进行打印操作,而此时1已经不再映射显示器,而是myfile
,随意此时打印的内容就到了myfile
中。
所以重定向的本质是上层用到的文件描述符不变,在内核中更改文件描述符映射的文件
。
系统调用接口实现重定向
我们可以通过上面的方法先close
再open
实现重定向,但不够优雅。
实际上操作系统也提供了实现重定向的接口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
会拷贝oldfd
到newfd
中,如果必要的时候会先关掉newfd
,但是还有两点需要注意的:
- 如果
oldfd
是无效的文件描述符,那么调用就会失败,原有的newfd
也不会关闭。 - 如果
oldfd
是有效的文件描述符,而newfd
跟oldfd
一样,也就是传进来的两个文件描述符是相同的,那么函数什么也不干。
此时有这样的映射关系:
然后我们试着调用dup2(4, 1)
,映射关系就会发生下面的变化:
此时再向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,是甲骨文公司的一款关系…...

【方案】基于AI边缘计算的智慧工地解决方案
一、方案背景 在工程项目管理中,工程施工现场涉及面广,多种元素交叉,状况较为复杂,如人员出入、机械运行、物料运输等。特别是传统的现场管理模式依赖于管理人员的现场巡查。当发现安全风险时,需要提前报告࿰…...

华为各型号交换机开启SNMP v3
设备型号:华为S5720S-28P-LI-AC 设备软件版本:V200R011C10SPC600 调试命令: snmp-agent snmp-agent sys-info version v3 snmp-agent group v3 GroupName privacy //{GroupName}是设置一个SNMP的组名,我设置是SNMPGroup snm…...

CocosCreator3.8研究笔记(一)windows环境安装配置
一、安装Cocos 编辑器 (1)、下载Cocos Dashboard安装文件 Cocos 官方网站Cocos Dashboard下载地址 : https://www.cocos.com/creator-download9下载完成后会得到CocosDashboard-v2.0.1-win-082215.exe 安装文件,双击安装即可。 …...

【JavaWeb 专题】15个最经典的JavaWeb面试题
文章目录 HTTP长连接和短连接HTTP/1.1 与 HTTP/1.0 的区别可扩展性缓存带宽优化长连接消息传递Host 头域错误提示 AjaxAjax 的优势: JSP 和 servlet 有什么区别?定义区别 JSP 的9大内置对象及作用JSP 的 4 种作用域?session 和 cookie 有什么…...

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

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

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

monorepo更新组件报错,提示“无法加载文件 C:\Program Files\nodejs\pnpm.ps1,因为在此系统上禁止运行脚本”
解决方法: 第一步:管理员身份运行 window.powershell, win x打开powerShell命令框,进入到对应项目路径。 第二步:执行:get-ExecutionPolicy,显示Restricted,表示状态是禁止的; 第…...

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

Android全面屏下,默认不会全屏显示,屏幕底部会留黑问题
前些天发现了一个蛮有意思的人工智能学习网站,8个字形容一下"通俗易懂,风趣幽默",感觉非常有意思,忍不住分享一下给大家。 👉点击跳转到教程 公司以前的老项目,便出现了这种情况,网上搜索了各种资料…...

5.Redis-string
string 字符串 字符串类型是 Redis 最基础的数据类型,关于字符串需要特别注意: 1.⾸先Redis中所有 key 的类型都是字符串类型,⽽且其他⼏种数据结构也都是在字符串类似基础上构建的,例如 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 设置与命令基础(二)
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 目录 前言 一、系统基本操作 二、命令类型 三、命令语法 四、命令补齐 五、命令帮助 六、系统基本操作命令 总结 前言 这是本人学习Linux的第二天,今天主…...

ubuntu20.04中ros2安装rosbridge及启动方式
ros2 启动rosbridge: 要启动ROS2中的rosbridge,需要先安装ROS2的rosbridge_suite软件包。使用以下命令安装: sudo apt-get update sudo apt-get install ros-<distro>-rosbridge-suite将<distro>替换为正在使用的ROS2发行版的名…...

TCP之超时重传、流量控制和拥塞控制
一、超时重传 TCP超时重传是TCP协议中的一种机制,用于在发生丢包或数据包未及时确认的情况下,重新发送未确认的数据段。 当发送方发送一个数据段后,会启动一个定时器(称为超时计时器),等待接收方的确认。…...

git clone 报SSL证书问题
git命令下运行 git config --global http.sslVerify false 然后再进行重新clone代码...

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

代码随想录打卡—day46—【DP】— 8.29 背包END
1 139. 单词拆分 139. 单词拆分 做了很久...估计2h 一开始我的思路卡死了 看题解之后的思路的详解见注释, 我的写法和carl 答案在一些微小的细节上略有不同,我的更好理解,但他的解法更简单。 我写的过程中,需要注意下标和字符…...

lua学习-3 循环和流程控制
这里写目录标题 判断for 循环数值遍历泛型遍历遍历数组遍历对象ipairs 和 pairs的异同 while 循环repeat循环goto基础用法注意事项 判断 for 循环 数值遍历 for exp1,exp2,exp3 do//todoend上述代码是指:从exp1 到exp2 以exp3为步长进行循环并执行todo代码&#…...

3、监测数据采集物联网应用开发步骤(3)
监测数据采集物联网应用开发步骤(2) 系统整体结构搭建 新建项目 输入项目名称:MonitorData 所谓兵马未动粮草先行,按下图创建好对应的模块备用: com.plugins 业务插件模块 com.zxy.adminlog 日志或文本文…...

MySQL用户管理及用户权限
目录 数据库用户管理 新建用户 查看用户 重命名用户rename 删除用户drop 修改用户密码 找回root密码 数据库用户授权 授予权限 查看用户权限 撤销用户权限 数据库用户管理 新建用户 CREATE USER 用户名来源地址 [IDENTIFIED BY [PASSWORD] 密码];用户名:…...

Yolov8-pose关键点检测:模型轻量化创新 | PConv结合c2f | CVPR2023 FasterNet
💡💡💡本文解决什么问题:新的partial convolution(PConv),通过同时减少冗余计算和内存访问可以更有效地提取空间特征。 PConv| GFLOPs从9.6降低至8.5,参数量从6482kb降低至6134kb, mAP50从0.921提升至0.925 Yolov8-Pose关键点检测专栏介绍:https://blog.csdn.n…...

聊聊mybatis-plus的SafetyEncryptProcessor
序 本文主要研究一下mybatis-plus的SafetyEncryptProcessor SafetyEncryptProcessor mybatis-plus-boot-starter/src/main/java/com/baomidou/mybatisplus/autoconfigure/SafetyEncryptProcessor.java public class SafetyEncryptProcessor implements EnvironmentPostProc…...

【PCL (Point Cloud Library)可视化点云的工具汇总】
PCL (Point Cloud Library)可视化点云的工具 PCL (Point Cloud Library) 提供了一系列的工具和类用于点云的可视化。以下是其中的一些主要工具和功能: pcl::visualization::CloudViewer: 如前所述,这是一个简单易用的可视化工具,主要用于基本的点云显示。pcl::visualizatio…...

实现 Trie (前缀树)
题目链接 实现 Trie (前缀树) 题目描述 注意点 word 和 prefix 仅由小写英文字母组成 解答思路 首先要理解前缀树是什么,参照该篇文章【图解算法】模板变式——带你彻底搞懂字典树(Trie树)在了解前缀树是什么后,设计前缀树就会更加容易,…...

ElasticSearch基础知识汇总
文章目录 前言一、认识ElasticSearch1.正向索引和倒排索引2. MySql与ElasticSearc3.IK分词器 二、ES索引库操作1.mapping映射属性2.索引库的CRUD 三、ES文档库操作 前言 Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基…...

服务器数据库中了locked勒索病毒怎么办,locked勒索病毒恢复工具
最近一段时间网络上的locked勒索病毒非常嚣张,自从6月份以来,很多企业的计算机服务器数据库遭到了locked勒索病毒的攻击,起初locked勒索病毒攻击用友畅捷通T用户,后来七月份开始攻击金蝶云星空客户,导致企业的财务系统…...