Re:Linux系统篇(二十九)文件篇·二:深度解析Linux文件描述符、dup2指针覆盖与内建命令重定向完全解析


◆ 博主名称: 小此方-CSDN博客 大家好,欢迎来到小此方的博客。
⭐️Linux系列个人专栏: 【主题曲】Linux
⭐️此方的GitHub: github_此方
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)


文章目录


概要&序論

  Hello大家好,我是此方。本文深刻探讨 Linux 文件描述符底层结构与重定向机制。

  • 文件描述符(fd)整型值本质与标准输入、输出、错误的绑定关系;
  • 内核 task_struct、文件描述符表 files_structstruct file 结构体的指针层级映射;
  • 详解文件描述符的"最小分配规则"及手动闭合标准流实现重定向的底层原理; dupdup2 系统调用对内核文件对象指针的覆盖与备份操作;
  • 完成自定义 Shell 的"超级升级",重定向。内核 struct file 的引用计数
    好的,我们直接开始。

一、文件描述符的概念

1.1文件描述符的基础认识

  我们上篇文章故意回避了一个问题:open系统调用的返回值是什么?答案是"文件描述符(new file descriptor) "。

  文件描述符是什么?我们打印它看一看:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
    umask(0);
    int fd1 = open("log1.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fd2 = open("log2.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fd3 = open("log3.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fd4 = open("log4.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    if(fd1 < 0||fd2 < 0||fd3 < 0||fd4 < 0)exit(1);
    printf("fd1: %d\n", fd1);printf("fd2: %d\n", fd2);
    printf("fd3: %d\n", fd3);printf("fd4: %d\n", fd4);
    close(fd1);close(fd2);close(fd3);close(fd4);
}

  我们发现,文件描述符是一个整型值 。但是有人会发现:为什么文件描述符是从3开始打印而不是从0开始打印?

  因为,我们上一篇文章也说了:"程序启动的时候为我们自动打开了三个流(文件),标准输入(0)、标准输出(1)、标准错误(2)。以C为例,stdin,stdout,和stderr"

1.2基于文件描述符的向上讨论

  回到我们C语言的文件操作,我想告诉大家:上篇文章我们讲到fopen函数底层封装了open函数,那么FILE返回值这个结构体里面一定也封装了一个fd文件描述符。

  使用指令打开:

bash 复制代码
view /usr/include/bits/types/struct_FILE.h

  这里的fileno就是我们要找的文件描述符。

在操作系统看来,它只认识文件描述符。 任何语言,不论它的顶层怎么设计,底层都封装了文件描述符。
以下全部都是基于文件描述符的向下讨论

  我刚才说文件描述符是一个数字,它是从0开始计数的,有没有想到什么?对,数组下标。文件描述符就是数下标。 从哪里开始讲呢?

二、文件描述符表

2.1文件描述符表的概念

  我们的系统在为我们创建进程的时候,除了task_struct、页表、虚拟地址空间等等。还创建了一个文件描述符表结构体,这个结构体里面有一个数组:文件描述符数组(这个数组是一个结构体指针数组

  我们找一个task_struct的源代码看看,嗯,确实有一个:

  文件描述符数组在源代码中:

  那么画一个简单的草图,就是这个结构:

  这个文件描述符数组里面存储的结构体是什么?是结构体struct file,我们找到它的源代码: (待会儿回来看它的东西有什么)

  当我们打开一个文件,操作系统会为我们创建一个文件描述结构体struct file,这个结构体中存放文件的属性 ,然后里面有一个指针指向一块文件缓冲区,这个文件缓冲区里面存放了这个文件的内容。

  将我们上面画的两张图拼在一起,结论出来了:

  文件描述符本质是文件描述符表这个结构体(struct file_struct)中的文件描述符数组的下标,这些下标指向一个个文件描述结构体(struct file),于是,拿到文件描述符就可以获取文件的描述结构体,就可以获得某个被打开的文件的全部信息。

2.2结合文件描述符底层重新理解I/O

对文件内容做任何的操作,都必须将文件内容加载(从磁盘到内存的拷贝)到内核中对应的文件缓冲区中。

read函数的本质是:从内核到用户空间的拷贝函数。

写也是一样,我不想画图了,就是将应用层的内容拷贝到缓冲区,然后操作系统再定期把内容刷新到磁盘里。

Tips:struct file里面有什么

  来看看这个struct file结构体里面有什么吧:什么着啊那的,先看看就行,不用管,后续的文章中再详细讲解。其中主要包含的内容比如:

cpp 复制代码
struct file
{
    //属性集合
    //int mode
    //读写位置
    //读写选项
    //缓冲区 --- TODO
    //操作方法
    //struct list_head list;
}

  文件描述结构体里面也有一个union来构建file与file之间的连接,方便管理("先描述再组织"无处不在

cpp 复制代码
	union {
		struct list_head	fu_list;     // 内核文件链表指针★
		struct rcu_head 	fu_rcuhead;  // RCU锁释放时的内存头
	} f_u;
cpp 复制代码
struct file {
	/*
	 * fu_list becomes invalid after file_free is called and queued via
	 * fu_rcuhead for RCU freeing
	 */
	union {
		struct list_head	fu_list;     // 内核文件链表指针★
		struct rcu_head 	fu_rcuhead;  // RCU锁释放时的内存头
	} f_u;
	
	struct dentry		*f_dentry;       // 【属性】指向目录项对象的指针(通过它能找到文件名、inode等)
	struct vfsmount         *f_vfsmnt;       // 【属性】指向文件系统挂载点的指针
    
	const struct file_operations	*f_op;   // 【操作方法】极其重要!指向该文件操作函数指针集合(如 read, write 等底层实现)
	
	atomic_t		f_count;         // 【属性】文件对象的引用计数(有多少个fd指向它,为0时文件才真正关闭)
	unsigned int 		f_flags;         // 【读写选项】打开文件时的标志(如 O_RDONLY, O_WRONLY, O_NONBLOCK)
	mode_t			f_mode;          // 【属性/int mode】文件的访问权限(如可读、可写)
	loff_t			f_pos;           // 【读写位置】当前文件的读写偏移量(即下一次 read/write 开始的字节位置)
	
	struct fown_struct	f_owner;         // 【属性】异步I/O时接收信号的进程属主信息
	unsigned int		f_uid, f_gid;    // 【属性】文件所有者的 User ID 和 Group ID
	struct file_ra_state	f_ra;            // 【属性】文件预读(Read-Ahead)状态流,用来优化磁盘读取性能

	unsigned long		f_version;       // 【属性】版本号,每次使用后会自动更迭
	void			*f_security;     // 【属性】安全模块(如 SELinux)使用的安全上下文指针

	/* needed for tty driver, and maybe others */
	void			*private_data;   // 【属性】私有数据指针(系统调用或驱动程序常用来挂载自定义的特殊结构体)

#ifdef CONFIG_EPOLL
	/* Used by fs/eventpoll.c to link all the hooks to this file */
	struct list_head	f_ep_links;      // 【属性】被 epoll 监听时,挂载到 epoll 事件等待队列的链表节点
	spinlock_t		f_ep_lock;       // 【属性】保护 epoll 链表的自旋锁
#endif /* #ifdef CONFIG_EPOLL */

	struct address_space	*f_mapping;      // 【缓冲区 - TODO】指向页高速缓存(Page Cache)映射的指针!
                                             // 这就是你上一张图说到的"内核开辟的文件缓冲区"在内核里的核心代理人。
};

三、重定向的原理

3.1 手动模拟重定向

3.1.1修改后产生的现象

  在理解了文件描述符的底层逻辑(也就是数组下标)之后,我们再来看操作系统分配文件描述符的规则:

最小的,没有被使用的,作为新的 fd 给用户。

  既然程序启动时默认打开了 0 (stdin)、1 (stdout)、2 (stderr),那如果我们故意把 1 给关闭了,然后再去打开一个新的文件,会发生什么?

  根据规则,操作系统一看,0 占着,1 空着,好,那就把 1 分配给新打开的文件!

  我们来看下面这段代码,手动模拟一下这个过程:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {
    close(1); // 故意关闭标准输出

    // 打开一个新文件,由于 1 被释放,根据最小分配原则,fd 必然是 1
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    
    // 此时往标准输出打印,看看会发生什么?
    printf("fd: %d\n", fd);
    
    // 注意:由于缓冲区问题,最后记得刷新或关闭
    close(fd);
    return 0;
}

  按道理说,printf 是往标准输出 里面打印东西的。但是当我们编译运行后,屏幕上什么都没有输出

  我们通过 ll 指令查看当前目录,发现多了一个 log.txt。打开这个文件一看,本该打印在屏幕上的 fd: 1 居然被写入到了 log.txt 里面!

3.1.2重定向的原理

  为什么会这样?

  因为 printf 只认 stdout,而 stdout 结构体内部封装的 fileno 就是 1。对应用层来说,"往 1 号下标对应的文件写数据"这一逻辑从未改变。

  但是在内核层,我们通过 close(1) 断开了 1 号下标指向标准显示器的连接,并让它重新指向了 log.txtstruct file

  上层不变底层变,上层还是原来的文件描述符,底层变成了别的地址,这------就是重定向的底层原理!

3.2 自动接口:dup2 系统调用

3.2.1接口介绍

  虽然通过先 closeopen 可以实现重定向,但这未免有点太笨拙了,而且不够优雅。为此,Linux 系统为我们提供了一个高效率的系统调用接口:dup2

  我们先来看一下它的系统调用手册:

bash 复制代码
# int dup2(int oldfd, int newfd);

RETURN VALUE:

On success, these system calls return the new descriptor. On error, -1 is returned, and errno is set appropriately.

  关于 dup2 的两个参数 oldfdnewfd,很多人第一次看文档极其容易搞混。官方文档有一句非常核心的解释:

dup2() makes newfd be the copy of oldfd, closing newfd first if necessary.

  这句话翻译过来就是:newfd 成为 oldfd 的一份拷贝。 如果 newfd 已经打开了,会先把它关闭。

  这里的"拷贝",拷贝的绝对不是数组下标的数字,而是文件描述符表数组项中的内容(即 struct file 的指针)

  简而言之,就是oldfd 指向的文件对象指针,覆盖到 newfd 的位置上 。最终的结果是:newfdoldfd 都指向了原来 oldfd 所指向的文件。

3.2.2实际应用

   如果要进行输出重定向(比如把本该输出到屏幕的内容重定向到文件中):

我们希望原本往 1 号(newfd)打的内容走到新打开的文件 fdoldfd)中去。所以代码应该写成:dup2(fd, 1);

  下面我们用 dup2 来重写一下上面的功能:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
    // 1. 正常打开文件,拿到一个普通的 fd(比如 3)
    int fd = open("myfile.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    if(fd < 0) {
        perror("open");
        exit(1);
    }
    // 2. 使用 dup2 进行输出重定向
    // 凡是往 1 号文件描述符写入的内容,都写到 myfile 当中,而不再写到标准输出!
    dup2(fd, 1); 
    // 3. 测试输出
    printf("凡是往1号文件描述符写的内容,都写到了myfile当中,而不再写到标准输出!\n");
    printf("fd: %d\n", fd);
    // 4. 关闭文件 //dup2它不回去主动关闭原来的文件
    close(fd);
    return 0;
}

四、升级自定义Shell:实现重定向功能

   我们在自定义Shell编写的那篇中讲到过,第四阶段的代码放在《文件篇》中讲解,它来了。我们要实现的是自定义Shell的"重定向功能"。

4.1 核心设计思路与宏定义

   要想让 Shell 支持重定向,我们需要把整个过程拆解为两步:

  1. 宏观检测: 在解析用户输入的命令行字符串时,检查其中是否包含 <>>> 等重定向符号。如果存在,必须将这些符号以及后面的文件名从原命令行中"拆离"出来。
  2. 底层替换:fork() 出子进程之后、程序替换 execvp() 之前,根据重定向的类型,调用 open() 系统调用打开对应的文件,并使用 dup2() 改变子进程的标准输入或标准输出。

   首先,我们在代码顶层定义代表重定向类型的 4 个宏状态,以及用于保存全局重定向状态和目标文件名的全局变量:

cpp 复制代码
#define NONE_REDIR   0  // 无重定向
#define INPUT_REDIR  1  // 输入重定向 <
#define OUTPUT_REDIR 2  // 输出重定向 >
#define APPEND_REDIR 3  // 追加重定向 >>

int redir = NONE_REDIR; // 记录当前重定向的类型
string filename;        // 记录重定向的目标文件名

4.2 文本解析:重定向检测

   由于用户的输入可能长这样:ls -a -l > log.txt。我们需要从字符串的末尾 开始向前扫描。因为重定向符号及其文件名一般都在命令行的最右侧。

   一旦扫描到符号,我们需要做两件事:

  • 将符号位置置为 \0,从而把前面的有效命令和后面的文件名阶段分隔开。
  • 跳过符号与文件名之间的空格,提取出纯净的文件名给 filename

   检测函数的核心实现如下:

cpp 复制代码
void RedirCheck(char* cmd)
{
    redir = NONE_REDIR;
    filename.clear();
    int end = strlen(cmd) - 1;
    
    // 从后往前扫描
    while (end >= 0)
    {
        if (cmd[end] == '<')
        {
            cmd[end] = '\0'; // 截断前半部分命令
            end++; 
            while (isspace(cmd[end])) end++; // 跳过可能存在的空格
            redir = INPUT_REDIR; 
            filename = cmd + end; // 拿到文件名
            break;
        }
        else if (cmd[end] == '>')
        {
            if (end > 0 && cmd[end - 1] == '>') // 检测到连续的 >>
            {
                cmd[end - 1] = '\0';
                end++;
                while (isspace(cmd[end])) end++;
                redir = APPEND_REDIR;
                filename = cmd + end;
            }
            else // 检测到单个 >
            {    
                cmd[end] = '\0';
                end++;
                while (isspace(cmd[end])) end++;
                redir = OUTPUT_REDIR;
                filename = cmd + end;
            }
            break;
        }
        end--;
    }
}

   很多人在写这段字符串解析时,会先把 cmd[end] = 0,紧接着在循环条件里写 while(isspace(cmd[end])) end++;。此时 cmd[end] 已经是 \0(字符串结束符)了,isspace('\0') 永远为假,导致循环直接跳出,提取的文件名里会夹杂着大量的非法空格或者错位指针,导致后续 open 失败,正确的做法是先将位置置空,然后将指针 end++ 移向后方,再去跳过空格

4.3 进程替换与重定向的完美结合

   文本解析好了,状态也保存了。接下来到了最核心的命令执行函数 Execute()

Tips:为什么一定要在子进程里做 dup2?

   这也是自定义 Shell 升级时最容易犯的原则性错误:绝对不能在父进程中执行重定向。

   如果我们在父进程(Shell 本身)里直接调用 dup2(fd, 1),那么完蛋了,后续你的 Shell 打印命令提示符 [user@host...]$ 时,提示符不会显示在屏幕上,而是全部被写进了你的重定向文件里!

   升级后的执行逻辑:

cpp 复制代码
void Execute()
{
    // 无论是普通命令还是重定向命令,统一先 fork
    pid_t id = fork();
    if (id == 0)
    {
        if (redir == INPUT_REDIR)
        {
            // 输入重定向:只读打开
            int fd = open(filename.c_str(), O_RDONLY);
            if(fd < 0) { perror("open"); exit(1); }
            dup2(fd, 0); // 覆盖标准输入
            close(fd);
        }
        else if (redir == OUTPUT_REDIR)
        {
            // 输出重定向:只写、无则创建、覆盖式刷新
            int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
            if(fd < 0) { perror("open"); exit(1); }
            dup2(fd, 1); // 覆盖标准输出
            close(fd);
        }
        else if (redir == APPEND_REDIR)
        {
            // 追加重定向:只写、无则创建、追加式写入
            int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
            if(fd < 0) { perror("open"); exit(1); }
            dup2(fd, 1); // 覆盖标准输出
            close(fd);
        }
        execvp(g_argv[0], g_argv);
        perror("execvp"); // 如果 execvp 失败,说明命令输入错误
        exit(1);
    }
    
    // 父进程只负责等待子进程,回收僵尸进程,获取退出码
    int status = 0;
    waitpid(id, &status, 0);
    if (WIFEXITED(status))
    {
        exitcode = WEXITSTATUS(status);
    }
}

4.4 完整的代码

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string>
#include <sys/stat.h>
#include <fcntl.h>
#include <cctype> 

using namespace std;

const int COMMAND_SIZE = 128;
const int LINE_SIZE = 1024;
#define PROMPT "[%s@%s %s]%s"

const int ENV_SIZE = 100;
int g_envs= 0;
char* g_env[ENV_SIZE] = {0};

#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3

int redir = NONE_REDIR;
string filename ;

int g_argc = 0;
char* g_argv[LINE_SIZE] = {0}; 

int exitcode = 0; 

const char* GetHostName()
{
	const char* host = getenv("HOSTNAME");
	return host == NULL? "None" : host ;
}

const char* GetUserName()
{
	const char* user = getenv("USER");
	return user == NULL? "None" : user ;
}

string GetDirectory(char* _cwd )
{
	if(_cwd == NULL) return " "; 
	string cwd = _cwd;
	int pos = cwd.rfind("/");
	if(pos == string :: npos )
		return " ";
	return cwd.substr(pos +1);
}

string GetCwd()
{
	char* cwd = getenv("PWD");
	return GetDirectory(cwd); 
}

const char* GetSign()
{
	if(strcmp(GetUserName(), "root") == 0) 
		return "#";
	else return "$";
}

const char* GetHome()
{
	const char* home =  getenv("HOME");
	return home ==NULL? "None" :home;
}

void MakeCommandPrompt(char* det , int size)
{
	snprintf(det,size, PROMPT ,GetUserName(),GetHostName(),GetCwd(),GetSign());
}

void PrintCommandPrompt()
{
	char Prompt[COMMAND_SIZE] = {0};
	MakeCommandPrompt(Prompt,sizeof(Prompt));
	printf("%s",Prompt);
	fflush(stdout);
}

bool GetCommandLine(char* cmd,int size )
{
	char* buf = fgets(cmd , size , stdin);
	if(buf==NULL) return false;
	cmd[(size_t)(strlen(cmd)-1)] = 0;
	if(strlen(cmd)==0) return false;
	return true;
}

void Initenv()
{
	extern char** environ;
	for(int i = 0; environ[i] ; i++)
	{
		g_env[i] = (char*)malloc(strlen(environ[i])+1);
		if(g_env[i]==NULL)
		{
			string enverror("环境变量初始化异常");
			throw enverror;
		}
		strcpy(g_env[i],environ[i]);
		g_envs++;
	}
	g_env[g_envs]=NULL;
	for(int i =0 ;g_env[i];i++)
		putenv(g_env[i]);
}

void RedirCheck(char* cmd)
{
	redir = NONE_REDIR;
	filename.clear();
	int start = 0;
	int end = strlen(cmd) - 1;
	while(end > start)
	{
		if(cmd[end]=='<')
		{
			cmd[end] = 0;
			end++; // 修复:先向后移动一位再跳过空格
			while(isspace(cmd[end]))
				end++;
			redir = INPUT_REDIR; 
			filename = cmd + end;
			break;
		}
		else if(cmd[end]=='>')
		{
			if(cmd[end-1]=='>')//>>
			{
				cmd[end-1] = 0;
				cmd[end] = 0;
				end++; 
				while(isspace(cmd[end]))
					end++;
				redir = APPEND_REDIR;
				filename = cmd + end;
			}
			else
			{	
				cmd[end] = 0;
				end++; 
				while(isspace(cmd[end]))
					end++;
				redir = OUTPUT_REDIR;
				filename = cmd + end;
			}
			break;
		}
		else end--;
	}
}

bool AnalyseCommandLine(char* cmd)
{
#define EXC " "
	g_argc = 0;
	g_argv[g_argc++] = strtok(cmd,EXC);
	while((bool)(g_argv[g_argc++]=strtok(NULL,EXC)));
	g_argv[g_argc] = NULL;
	g_argc--;
	if(g_argc==0)
		return false;
	return true;
}

void Cd()
{
	if(g_argc == 1)
		chdir(GetHome());
	else if (g_argc == 2){
		if(strcmp(g_argv[1] ,"~") == 0)
			chdir(GetHome());
		else if(strcmp(g_argv[1],"-")==0)
		{
			const char* oldpwd = getenv("OLDPWD");
			if(oldpwd) chdir(oldpwd);
		}
		else chdir(g_argv[1]);
	}
	else{
		string cderror("cd命令执行错误");
		throw cderror;
	}

	// 维护 PWD 环境变量,防止 cd 后提示符不更新路径
	char cwd_buf[LINE_SIZE];
	if (getcwd(cwd_buf, sizeof(cwd_buf))) {
		setenv("PWD", cwd_buf, 1);
	}
}

void Echo()
{
#define MAX_SIZE_ENV 1000
	if(g_argv[1] == NULL)
	{
		cout << endl;
	}
	else if(strcmp(g_argv[1], "$?") == 0) 
		cout<<exitcode<<endl;
	else if(g_argv[1][0] == '$')
	{
		const char* tmp = getenv(g_argv[1] + 1); 
		if(tmp) cout<< tmp << endl;
		else cout << endl;
	}
	else 
		printf("%s\n",g_argv[1]);
}

bool BuildinCommandCheck()
{
	if(strcmp(g_argv[0] ,"cd") == 0)
	{
		Cd();
		return true;
	}
	else if(strcmp(g_argv[0] , "echo") == 0)
	{
		Echo();
		return true;
	}
	return false; 
}

void Execute()
{
	pid_t id = fork();
	if(id == 0)
	{
		if(redir == INPUT_REDIR)
		{
			int fd = open(filename.c_str(), O_RDONLY);
			dup2(fd,0);
			close(fd);
		}
		else if(redir == OUTPUT_REDIR)
		{
			int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
			dup2(fd,1);
			close(fd);
		}
		else if(redir == APPEND_REDIR)
		{
			int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
			dup2(fd,1);
			close(fd);
		}

		execvp(g_argv[0], g_argv);
		exit(1); // 如果 execvp 失败,子进程安全退出
	}

	int statue = 0;
	waitpid(id, &statue, 0);
	if (WIFEXITED(statue)) {
		exitcode = WEXITSTATUS(statue);
	}
}

int main()
{
	try 
	{
		Initenv();
	}
	catch(string error)
	{
		cout<<error<<endl;
	}
	try
	{
		while(true)
		{
			//打印命令行提示符
			PrintCommandPrompt();
			//获取命令行参数
			char commandline[LINE_SIZE] = {0};
			if(!GetCommandLine(commandline ,sizeof(commandline)))
				continue;
			//检测重定向类型
			RedirCheck(commandline);
			//分析命令行参数
			if(!AnalyseCommandLine(commandline))
				continue;	
			//检测是否是内建命令
			if(BuildinCommandCheck())
				continue;
			//执行命令
			Execute();
		}
	}
	catch (string error)
	{
		cout<<error<<endl;
	}
	catch(...)
	{
		cout<<"未知异常"<<endl;
	}
	return 0;
}

五、加餐补充内容

5.1自定义Shell如何重定向内建命令

  在完成了自定义 Shell 的重定向升级后,你可能会发现一个隐藏的致命问题:如果用户输入的是内建命令,应该如何重定向?

  因此,内建命令的重定向,核心关键在于:重定向操作完成后,如何将底层指针恢复如初?

  为了解决这个问题,我们需要引入另一个系统调用:dup

5.1.1 认识 dup 系统调用

  与 dup2 强制覆盖指定下标不同,dup 的作用是复制一份标准指针到当前最小的空闲位置

cpp 复制代码
#include <unistd.h>
int dup(int oldfd);

返回值: 成功时返回新分配的文件描述符(该描述符指向与 oldfd 相同的文件对象);失败返回 -1。

  利用 dup 恢复指针:

  1. 备份: 在对 1 号(stdout)进行重定向覆盖之前,先调用 int save_stdout = dup(1);。此时,系统会分配一个新 fd(比如 3),让 3 号也指向标准显示器。这样我们就把显示器的指针备份到了 3 号位置
  2. 重定向: 调用 dup2(fd, 1);,放心地让 1 号指向目标文件,供内建命令使用。
  3. 执行: 调用内建命令的执行函数(如 Echo())。
  4. 恢复: 内建命令执行完毕后,调用 dup2(save_stdout, 1);。把刚才备份在 3 号的显示器指针重新覆盖回 1 号位置!
  5. 善后: 调用 close(save_stdout);close(fd);,释放不需要的临时描述符。

5.2 代码层面的完美适配

  修改后的内建命令处理逻辑如下:

cpp 复制代码
bool BuildinCommandCheck(){
    // 如果没有内建命令命中,直接返回 false
    if (strcmp(g_argv[0], "cd") != 0 && strcmp(g_argv[0], "echo") != 0){
        return false;
    }
    int save_stdin = -1;
    int save_stdout = -1;
    if (redir == INPUT_REDIR){
        save_stdin = dup(0); // 备份原标准输入
        int fd = open(filename.c_str(), O_RDONLY);
        if(fd >= 0) {
            dup2(fd, 0); // 重定向标准输入
            close(fd);
        }
    }
    else if (redir == OUTPUT_REDIR){
        save_stdout = dup(1); // 备份原标准输出
        int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
        if(fd >= 0) {
            dup2(fd, 1); // 重定向标准输出
            close(fd);
        }
    }
    else if (redir == APPEND_REDIR){
        save_stdout = dup(1); // 备份原标准输出
        int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
        if(fd >= 0) {
            dup2(fd, 1); // 重定向标准输出
            close(fd);
        }
    }
    if (strcmp(g_argv[0], "cd") == 0)
        Cd();
    else if (strcmp(g_argv[0], "echo") == 0)
        Echo();
        //关键:恢复
    if (save_stdin != -1){
        dup2(save_stdin, 0); // 恢复标准输入
        close(save_stdin);   // 释放备份
    }
    if (save_stdout != -1){
        dup2(save_stdout, 1); // 恢复标准输出
        close(save_stdout);    // 释放备份
    }
    return true; 
}

5.2 文件与引用计数

  我们在前面的草图中提到了系统的核心原则:一个文件可以被多个进程同时打开。这就带来了一个问题:当某个进程关闭一个文件时,操作系统怎么知道该不该把这个文件从内核中真正释放掉?

5.1.1引用计数的增减规则

  在内核的 struct file 结构体内部,有一个名为 f_count 的成员(即引用计数)。它的工作机制:

  • 增加: 每当有一个新的文件描述符(fd)指针指向该文件(如通过 openfork 进程继承,或调用 dup系列接口拷贝指针),该文件的引用计数就会 ++
  • 减少: 每当用户层调用 close(fd),或者进程退出导致描述符表被销毁时,操作系统并不会直接抹除该文件,而是先让对应的引用计数 --
  • 关闭: 当文件的引用计数为0的时候,文件才会被正式关闭。

  有人会问:为什么struct file_struct里面也有一个引用计数?这个没法讲,得等到线程章节再说,你就记住一句话:"这个引用计数决定文件描述符表何时真正被销毁"


好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye!

相关推荐
wuminyu2 小时前
Java锁机制之park与futex系统级协同机制解析
java·linux·c语言·jvm·c++
Cosolar5 小时前
LlamaIndex索引类型全解析:原理与实战指南
运维·服务器
方便面不加香菜7 小时前
Linux--基础IO(一)
linux·运维·服务器
鼎讯信通9 小时前
风电光缆运维提质增效:G-4000A 光缆故障追踪仪破解风场巡检难题
运维·网络·数据库
三十..10 小时前
MySQL 从入门到高可用架构实战精要
运维·数据库·mysql
跨境数据猎手11 小时前
大数据在电商行业的应用
大数据·运维·爬虫
linyanRPA11 小时前
影刀RPA店群自动化实战:多店铺活动自动报名与促销管理架构设计
运维·自动化·办公自动化·rpa·python脚本·爬虫自动化·店群自动化
mounter62512 小时前
现代 Linux 内存管理的演进与变革:从传统 LRU 到多代架构 MGLRU
linux·服务器·kernel
会Tk矩阵群控的小木12 小时前
安卓群控系统对于游戏工作室实战教程
android·运维·游戏·adb·开源软件·个人开发