Linux系统与系统编程(9)——自设计shell与基础IO

前言

**欢迎观看Linux系列文章!!**第9篇主要讲述了如何设计一个简单shell、基础IO的系统文件IO、文件描述符、重定向和缓冲区相关知识。

自设计shell

该篇的目的是为了总结之前学到的Linux知识,完成的shell只是一个很简单的程序。

输出命令提示符

代码如下

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

#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "

//获取USERNAME
const char* GetUserName()
{
	const char* name = getenv("USER");
	return name == NULL ? "NONE" : name;
}
//获取HOSTNAME
const char* GetHostName()
{
	const char* hostname = getenv("HOSTNAME");
	return hostname == NULL ? "NONE" : hostname;
}
//获取PWD
const char* GetPwd()
{
	const char* pwd = getenv("PWD");
	return pwd == NULL ? "NONE" : pwd;
}
//处理Pwd,只保留当前目录,去除上级目录.
#define SLASH "/"
std::string DirName(const char* pwd)
{
	std::string dir = pwd;
	if(dir == SLASH) return SLASH;
	auto pos = dir.rfind(SLASH);
	if(pos == std::string::npos) return "BUG?";
	return dir.substr(pos + 1);
}
//构造一个命令行提示符
void MakeCmdLine(char cmd_prompt[], int size)
{
	snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
}
//打印输出命令行提示符
void PrintCmdPrompt()
{
	char prompt[COMMAND_SIZE];
	MakeCmdLine(prompt, sizeof(prompt));
	printf("%s", prompt);
	fflush(stdout);
}

int main()
{
	while(true)
	{
		//1.输出命令行提示符
		PrintCmdPrompt();

	return 0;
}

效果如图:

获取用户命令与命令分析

演示代码:

cpp 复制代码
······

//shell全局数据
#define MAXARGC 128
char* g_argv[MAXARGC];//命令行参数表
int g_argc = 0;

······

//获取用户输入的命令行
bool GetCmdLine(char* out, int size)
{
	char *c = fgets(out, size, stdin);
	if(c == NULL) return false;
	out[strlen(out)-1] = 0;
	if(strlen(out) == 0) return false;
	return true;
}
//命令行分析
#define SEP " "
bool CmdParse(char* const cmdline)
{
	g_argc = 0;
	g_argv[g_argc++] = strtok(cmdline, SEP);
	while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));//这样可以把nullptr也放在参数表中
	g_argc--;
	return g_argc > 0 ? true : false;
}
//输出命令行参数表
void PrintArgv()
{
	for(int i = 0; g_argv[i]; i++)
	{
		printf("argv[%d]->%s\n", i, g_argv[i]);
	}
	printf("argc == %d\n", g_argc);

}
int main()
{
	while(true)
	{
		//1.输出命令行提示符
		PrintCmdPrompt();
		//2.获取用户输入的命令
		char cmdline[COMMAND_SIZE];
		if(!GetCmdLine(cmdline, sizeof(cmdline)))
		{
			continue;
		}
		//3.命令行分析
		if(CmdParse(cmdline))
        {
            continue;
        }
		PrintArgv();
	}
	return 0;
}

使用strtok分割用户输入的命令,然后把结果放在参数表g_argv中。

效果如图:

执行命令

演示代码:

cpp 复制代码
//4.执行命令
pid_t id = fork();
if(id == 0)
{
    execvp(g_argv[0], g_argv);
    exit(0);
}
    pid_t rid = waitpid(id, NULL, 0);
    (void)rid;//没用的代码,只是为了避免警告

该部分紧接着放在命令行解释代码的下面。

效果如图:

执行内建命令

但是有些命令是内建命令,内建命令需要父进程shell完成,如cd,pwd,export等。因为子进程执行内建命令无法影响到父进程,而改变工作路径、改变环境变量等命令需要对shell本身也产生影响。shell完成内建命令,通过继承环境变量可以影响到子进程。

cpp 复制代码
······
//模拟一个环境变量表
char cwd[1024];
char cwdenv[1024];

·······

bool Cd()
{
	if(g_argc == 1)
	{
		std::string home = GetHome();
		if(home.empty()) return true;
		chdir(home.c_str());
	}
	else
	{
		std::string where = g_argv[1];
		if(where == "-")
		{}
		else if(where == "~")
		{}
		else
		{
			chdir(where.c_str());
		}
	}
	return true;
}

bool Echo()
{
    if(g_argc == 2)
	{
        std::string opt = g_argv[1];
		if(opt == "$?")
		{
			std::cout << lastcode << std::endl;
			lastcode = 0;
			return true;
		}
		else if(opt[0] == '$')
		{
			std::string env_name = opt.substr(1);
			const char* env_value = getenv(env_name.c_str());
			if(env_value)
			std::cout << env_value << std::endl;
		}
        else
        {
           std::cout << opt << std::endl;
        }
    }
    return true;
}

//检查并执行内建命令(这里只演示cd和echo $命令)
bool CheckAndExecBuiltin()
{
	std::string cmd = g_argv[0];
	if(cmd == "cd")
	{
		Cd();
		return true;
	}
	else if(cmd == "echo")
	{
		Echo();
		return true;
	}
	return false;
}


//执行命令
int Execute()
{
	pid_t id = fork();
	if(id == 0)
	{
		//子
		execvp(g_argv[0], g_argv);
		exit(0);
	}
	//父
	pid_t rid = waitpid(id, NULL, 0);
	(void)rid;//没用的代码,只是为了避免警告
	return 0;
}
int main()
{
	while(true)
	{
		//1.输出命令行提示符
		PrintCmdPrompt();
		//2.获取用户输入的命令
		char cmdline[COMMAND_SIZE];
		if(!GetCmdLine(cmdline, sizeof(cmdline)))
		{
			continue;
		}
		//3.命令行分析
	   if(!CmdParse(cmdline))
		{
			continue;
		}
		//PrintArgv();
		//4.检测并处理内建命令
		if(CheckAndExecBuiltin())
		{
			continue;
		}
		//5.执行命令
		Execute();
	}
	return 0;
}

这里先检测是否为内建命令(代码中只以cd作为演示)。若是,则不创建子进程完成命令,而是shell来完成。这里的cd命令使用了chdir系统调用函数来改变工作路径,同时使用putenv导入新的PWD环境变量,这样一来,子进程就可以通过继承新的PWD来改变旧的PWD了。

获取环境变量表

shell的环境变量是通过shell脚本从系统中获取的,我们自设计的shell,就从这个父进程shell中获取即可。

cpp 复制代码
·······

//环境变量表
#define Max_ENVS 100
char *g_env[Max_ENVS];
int g_envs = 0;

······

void InitEnv()
{
	extern char** environ;
	memset(g_env, 0, sizeof(g_env));
	g_envs = 0;
	//获取环境变量
	for(int i = 0; environ[i]; i++)
	{
		//申请空间
		g_env[i] = (char*)malloc(strlen(environ[i])+1);
		strcpy(g_env[i], environ[i]);
		g_envs++;
	}
	g_env[g_envs++] = (char*)"TESTENV=testing";
	g_env[g_envs] = NULL;
	//导入环境变量
	for(int i = 0; g_env[i]; i++)
	{
		putenv(g_env[i]);
	}
}

这样获取到的环境变量是全局的。

基础IO

理解文件

狭义上

文件在磁盘中;

磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的;

磁盘是外设;

磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出,即IO。

广义上

Linux下,一切皆文件。

文件操作的归类认知

0KB大小的文件也要占用磁盘空间;

文件是文件属性和文件内容的集合(文件 = 属性(元数据) + 内容);

所有文件操作本质上是文件内容操作和文件属性操作。

系统角度

对文件操作本质是进程对文件操作;

磁盘的管理者是操作系统;

文件的读写本质不是通过C/C++的库函数来操作的,而是通过文件相关的系统调用函数完成,这些库函数只是对这些系统调用的封装。

C语言文件接口

cpp 复制代码
#include<stdio.h>
#include<string.h>
int main()
{
	FILE* fp = fopen("log.txt", "w");
	if(fp == NULL)
	{
		perror("fopen");
	}

	int cnt = 1;
	while(cnt <= 10)
	{
		char buffer[1024];
		snprintf(buffer, sizeof(buffer), "msg%d\n", cnt++);

		fwrite(buffer, strlen(buffer), 1, fp);
	}
	
	fclose(fp);
	return 0;
}

这里只简单回顾C语言的文件接口,具体知识可以在别处学。

fopen的选项:

r

只读打开文本文件。文件必须已存在。流指针位于文件开头。

r+

读写打开文本文件。文件必须已存在。流指针位于文件开头。可读可写。

w

只写打开文本文件。如果文件不存在则创建;如果存在则将其长度截断为 0(清空内容)。流指针位于文件开头。

w+

读写打开文本文件。文件不存在则创建,存在则清空。流指针位于文件开头。

a

追加打开文本文件(只写末尾)。文件不存在则创建。流指针位于文件末尾,所有写入都追加到结尾。

a+

读 + 追加打开文本文件。文件不存在则创建。读取可以从文件开头开始,但写入永远追加到文件末尾。

系统文件I/O

传递标记位的一种方法

通过位图来传递,将一个整数对不同宏进行按位&,结果大于0的,就相当于传递了一个对应的宏。

open函数

打开文件

头文件和语法:

cpp 复制代码
#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,标志位。有如下这些标志,这些标志实际上都是宏,数值大小都是2的幂,也就是都只有一个位上是1,不同的标志,这个1的位置也不一样。

标志 含义
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读写打开
O_CREAT 若文件不存在则创建,此时必须提供 mode 参数
O_EXCL O_CREAT 共用,若文件已存在则打开失败(原子检测)
O_TRUNC 若文件存在且以写方式打开,清空内容写入
O_APPEND 每次写入前将文件偏移移到末尾(追加写入)
O_NONBLOCK / O_NDELAY 以非阻塞方式打开(用于设备、管道等)
O_SYNC 写入操作等待数据和元数据都落盘后才返回
O_DIRECTORY 若路径不是目录则打开失败

传参时,将需要功能对应的标志进行组合按位或 | ,就能得到一个整数,将这个整数作为flag传入函数内部。

使用时,在内部将flag与每个标志分别进行按位与 &,如果结果不为0,说明flags包含了这个标志。

这种做法叫做位图。

mode,权限码,这在之前权限的部分讲过,通过权限码来给创建的文件设置权限。只有在创建文件的时候才可以带上这个权限码。设置权限的时候还要经过系统的权限掩码umask计算。

返回值 ,打开文件的文件描述符。**其中0,1,2是默认的文件描述符,0表示标准输入stdin、1表示标准输出stdout、2表示标准错误stderr。**而FILE类型是C语言提供的一个结构体,它封装了文件描述符。

示例:

cpp 复制代码
int fd = open("log.txt", O_CREAT | O_WRONLY, 0666);

这个权限掩码可以程序里面设置

cpp 复制代码
umask(0);

这段代码把umask掩码设置为了0,这里设置的掩码不会影响到系统内部的掩码,程序根据就近原则使用这里新设置的掩码。

当open函数成功之后,返回一个文件描述符fd;失败就返回-1,并且设置一个错误码。

write函数

写操作

头文件和语法:

cpp 复制代码
#include <unistd.h>

ssize_t write (int fd, const void *buf, size_t count);

fd,open文件的时候返回的文件描述符。

buf, 写入的内容。因为类型是void,所以什么类型的数据都可以往里面写,都已二进制写入。所以写入字符时,因为字符的二进制数据可以被Vim解析,所以可以看到有意义的字符串,可是如果是其他类型的数据,写入之后Vim无法解析,看到的就是一堆乱码。

count,将要写入内容的大小。

返回值,实际写入内容的大小。错误时返回-1,并设置错误码。

示例:

cpp 复制代码
const char* msg = "hello world\n";
write (fd, msg, strlen(msg));

这里计算长度不需要把 \0 算进去,因为 \0 是C语言的语法规定,不是文件系统的,所以不用操心 \0 ,直接写入内容即可。

写操作的效果会受open获得的标志影响,若带上O_TRUNC,每次写都会从文件开头写,会覆盖原来的内容;若带上O_APPEND,就从文件末尾写,不会覆盖原来的内容。

read函数

读操作

头文件与语法:

cpp 复制代码
#include<unistd.h>

ssize_t read(int fd, void *buf, size_t count);

fd,open文件的时候返回的文件描述符。

buf, 接收读取内容的字符串。因为类型是void,所以什么类型的数据都可以往里面写,都已二进制写入。所以写入字符时,因为字符的二进制数据可以被Vim解析,所以可以看到有意义的字符串,可是如果是其他类型的数据,写入之后Vim无法解析,看到的就是一堆乱码。

count,将要读取内容的大小。

返回值,实际读取内容的大小。错误时返回-1,并设置错误码。

示例:

cpp 复制代码
char buffer[64];
int n = read(fd, buffer, sizeof(buffer) -1);

close函数

关闭文件

cpp 复制代码
close(fd);

关闭对应文件描述符fd的文件。

系统角度下的文件

文件描述符的本质

同PCB,当进程打开文件,就会创建一个结构体struct file,里面存放了文件的属性(权限、读写位置、读写选项、操作方法、缓冲区指针······)。

内部有一个指针指向文件缓冲区。文件的内容放到缓冲区中供程序访问(缓冲区后面讲)。

所有打开的文件就以链表的方式组织起来,对文件的操作就转变为对这个链表的操作了。

PCB内部有一个指针指向一个指针数组,叫作文件操作符表,存放的指针类型是struct file*,存放的指针指向该进程打开的文件,从而标识文件都由哪个进程打开。

所以文件描述符本质是一个数组下标。

文件描述符的分配

从未被分配的最小文件描述符开始分配。如果你把默认描述符0,1,2一开始就关闭了,那你再打开新的文件,被分配的文件描述符就从0,1,2开始。

案例:

我们把fd==1给关闭,就是关闭了标准输出stdout。然后打开一个文件,这个文件的描述符就是1。当我们再使用printf的时候,打印的内容是打印到文件描述符1的文件中。所以此时显示器上不会打印内容,原本要打印的内容就打印到了文件里。

重定向原理

上文的"案例"就是一种重定向操作。重定向就是改变文件描述符表的指针指向。

shell中重定向

输出重定向:将fd(1)重定向到指定文件

> ------ 覆盖输出到文件

cpp 复制代码
ls - l -a > log.txt

ls命令输出内容重定向到log.txt中。文件不存在就创建,存在就清空。

>>------ 追加输出到文件

cpp 复制代码
ls - l -a >> log.txt

与 > 类似,但是不会清空覆盖文件,而是追加方式输出到文件。

输入重定向:将fd(0)重定向到指定文件

< ------ 从文件中读取

bash 复制代码
wc -l < data.txt
代码中重定向

重定向核心函数:

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

成功,返回新的文件描述符;失败,返回 -1,并设置错误码。

作用是把oldfd覆盖到newfd上,newfd就作为oldfd的一份拷贝。此时newfd和oldfd相等,指向相同的文件。

理解一切皆文件

这有明显的好处:开发者仅需要一套API和开发工作,即可调取Linux系统汇总绝大部分的资源。

· 除了常见的文件,如C语言文件,图片,txt文档,可执行文件的等,设备也会被当成文件处理。

· 在底层,不同的设备有不同的属性,底层会为不同的设备创建一个结构体device存放基本信息(设备种类、设备状态、其他属性)。不同的设备还有不同的读方法和写方法,但他们的函数指针类型命名,参数,都是一样的。

· 通过虚拟文件系统VFS,创建一个struct file(底层的文件结构体),该结构体内存放了读方法函数指针和写方法函数指针。他们指向设备device的读写方法,通过同一个函数指针实现不同设备的不同调用方法。

· 进程访问设备时,就访问对应的file,file中有标识会标识设备文件。

· 读写时通过函数指针调用不同设备不同的读写方法。

· 在C++中是通过父子类继承和多态来实现的。

总而言之,步骤如下:

进程调用读写函数

>>>> VFS中找到设备文件

>>>> 识别为设备文件并提取设备号

>>>> 更具设备号找到驱动

>>>> 通过驱动找到读写操作函数

>>>> 使用硬件设备完成操作

缓冲区

什么是缓冲区

是内存的一部分,用来缓冲输入或输出的数据,这部分预留的空间叫作缓冲区。

根据对应的输入或输出设备,分为输入缓冲区和输出缓冲区。

该图展示的是缓冲区的简单框架。

我们常说的缓冲区,其实是用户级语言层缓冲区。

这个缓冲区在FILE结构体中,就是fopen函数的返回值。当我们打开一个文件,C语言库会malloc一块空间,作为用户级缓冲区。

在把用户级缓冲区的数据拷贝给OS之后,会有更复杂的操作,这里不多赘述。我们可以认为,当我们把数据拷贝给OS相当于拷贝给了硬件,拷过去之后就让OS自行完成剩下操作即可。

缓冲区刷新策略

即上图所说的"刷新条件"

①立即刷新 ------ 无缓冲 ------ 写透模式 WT(无条件刷新)

②满了刷新 ------ 全缓冲

③满行刷新 ------ 行刷新

内核缓冲区的刷新策略更复杂,这里就不多说。

缓冲区的作用

核心目的:提高效率

若用写透模式WT,就要多次调用系统调用函数,而调用系统调用是要消耗系统资源的。

用户级语言层缓冲区和文件内核缓冲区的关系,就像是家里的垃圾桶和小区楼下的垃圾桶一样。每次产生垃圾都跑下楼扔,比先扔家里的垃圾桶,满了再下楼要费劲更多。

全缓冲比行缓冲要更有效率,行缓冲比全缓冲更有实时性。所以一般情况下,普通文件使用的是全缓冲,显示器使用的是行刷新。

❤~~本文完结!!感谢观看!!接下来更精彩!!欢迎来我博客做客~~❤

相关推荐
IMPYLH2 小时前
Linux 的 unexpand 命令
linux·运维·服务器·bash
想唱rap2 小时前
IO多路转接之poll
服务器·开发语言·数据库·c++
|_⊙2 小时前
Linux 文件知识 补充
linux·运维·服务器
落羽的落羽3 小时前
【算法札记】练习 | Week4
linux·服务器·数据结构·c++·人工智能·算法·动态规划
Mortalbreeze3 小时前
深度理解文件系统 ---- 从磁盘存储到内核存储
大数据·linux·数据库
сокол4 小时前
【网安-Web渗透测试-内网渗透】域环境权限维持
服务器·windows·网络安全·系统安全
十六年开源服务商4 小时前
2026服务器配置优化与WordPress运维实战指南
android·运维·服务器
LN花开富贵6 小时前
Ubuntu aarch64 架构安装 NoMachine 远程控制 避坑与实战
linux·运维·笔记·学习·ubuntu·嵌入式
取经蜗牛6 小时前
Windows 11 WSL + Ubuntu 24.04 安装指南
linux·windows·ubuntu