linux笔记归纳5:进程控制

进程控制

一、进程创建

目录

进程控制

一、进程创建

1.1.fork函数

1.2.fork函数的返回值

1.3.写时拷贝

1.4.fork常规用法

1.5.fork调用失败的原因

二、进程终止

2.1.进程退出场景

2.2.进程退出码

2.3.进程退出方式

[2.4.exit VS _exit](#2.4.exit VS _exit)

三、进程等待

3.1.进程等待的必要性

3.2.进程等待的方法

3.3.退出状态(status)

3.4.退出信号与异常终止

3.5.阻塞与非阻塞等待

四、进程程序替换

4.1.程序替换原理

4.2.替换函数

4.2.putenv函数

4.3.execve函数

五、自主Shell命令行解释器

5.1.Myshell代码


1.1.fork函数

从已经存在的进程中创建一个新的进程

**原进程:**父进程

**新进程:**子进程

cpp 复制代码
#include <unistd.h>
pid_t fork(void);
返回值:⼦进程中返回0,⽗进程返回⼦进程PID,出错返回-1

进程调用fork,内核中的操作:

  • 分配新的内存块和内核数据结果给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程的列表当中
  • fork返回,调度器开始调度

1.2.fork函数的返回值

**子进程:**返回0

**父进程:**返回子进程的PID

1.3.写时拷贝

父子进程代码共享,当父子没有写入时,数据也是共享的

当任意一方试图写入,发生写时拷贝,各自生成一份副本

基本原理:

创建子进程时,操作系统会将页表的数据项设为只读

当写入数据时,操作系统会通过报错的形式

来重新申请内存,拷贝数据,建立映射关系

1.4.fork常规用法

创建子进程,让父子进程各自执行后续代码的一部分

创建子进程,让子进程执行全新的程序

1.5.fork调用失败的原因

系统中的进程过多,内存空间不足

实际用户的进程超过了限制

二、进程终止

**进程终止的本质:**系统少了个进程,释放PCB,进程地址空间,页表,代码和数据

(注:僵尸进程在终止时,它的PCB会被维护,方便父进程获取退出信息)

2.1.进程退出场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

子进程是由父进程创建的,子进程退出后,父进程接收子进程执行的结果,根据结果进行决策

main函数的返回值通常表示该进程的执行情况

**情况1:**代码运行完毕,结果正确,返回0

**情况2:**代码运行完毕,结果不正确,返回非0(不同的值表示结果不正确的原因)

进程退出后main函数的返回值会被写到PCB内部

父进程bash通过系统调用函数获取,写入status

main函数的返回值 == 进程正常退出时的退出码

**实验:**打印出最近一个进程退出时的退出码

  • proc.c

实验现象

2.2.进程退出码

**进程执行完毕,结果正确:**0

**进程执行完毕,结果不正确:**非0值

**实验:**打印退出码对应的描述

  • proc.c

实验现象

在Linux系统中,一共有134个默认的退出码

status可以存8位退出码,可以自定义121个

**实验:**打印打开文件不存在时,对应的标准退出码errno

  • proc.c

实验现象

查看使用指令ls打开不存在文件时的描述与标准退出码

如果进程代码能够运行完毕,可以返回定制的退出码

  • proc.c

实验现象

如果进程代码发生异常被信号杀死,只能返回128+信号值

  • proc.c

实验现象

进程出现异常时 ,是进程收到信号,此时status的15~8位数据为垃圾值,退出码无意义

2.3.进程退出方式

**方式一:**main函数返回,表示进程结束(其他函数返回,表示调用完成)

main正常返回后,返回值会当作该进程的退出码,作为exit函数的参数

**方式二:**调用exit函数

  • proc.c

实验现象

任何地方调用exit函数,表示进程结束

将子进程的退出码返回给父进程bash

**方式三:**调用_exit函数

  • proc.c

实验现象

2.4.exit VS _exit

exit(C语言的库函数)

cpp 复制代码
#include <stdlib.h>
void exit(int status);
参数:status 只定义了进程的退出码

换行刷新缓冲区,打印消息,2s后进程退出

消息先被放在缓冲区,2s后进程退出,自动刷新缓冲区,打印消息

_exit(系统调用函数)

cpp 复制代码
#include <unistd.h>
void _exit(int status);
参数:status 只定义了进程的退出码
(注:只有低8位可以使用:_exit(-1) == 255)

换行刷新缓冲区,打印消息,2s后进程退出

消息先被放在缓冲区,2s后进程退出,不会刷新缓冲区,没有打印消息

库函数系统调用函数 属于上下层之间的关系

库函数会调用相关的系统调用函数来完成任务

可以判断缓冲区一定不是操作系统内部缓冲区,而是C语言提供的库缓冲区

三、进程等待

3.1.进程等待的必要性

子进程退出,如果父进程不处理,子进程变成僵尸进程 ,造成内存泄漏 的问题并且无法被杀死

通过进程等待,父进程可以:

  • 回收子进程的资源(核心)
  • 获取子进程退出信息(可选)

3.2.进程等待的方法

wait方法

等待任意一个退出的子进程

cpp 复制代码
#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int* status);

返回值:成功返回目标僵尸进程PID,失败返回-1

参数:退出状态,不关⼼可以设置为NULL

**实验:**回收子进程的资源,解决僵尸进程问题

  • proc.c

实验现象

父进程等待子进程,在子进程没有退出时,父进程会阻塞在wait调用处

waitpid方法

cpp 复制代码
pid_ t waitpid(pid_t pid, int *status, int options);

返回值:

正常情况:返回收集到的子进程PID
没有已退出的⼦进程可收集:返回0
调用出错:返回-1 (errno会被设置成相应的值,指示错误)

参数:

1.pid

pid = -1:等待任意一个子进程
pid > 0:等待与输入的PID相等的子进程

2.status(输出型参数)

/*宏函数*/
WIFEXITED(status):  查看进程是否是正常退出,非0表示正常退出(检测信号值是否为0)
WEXITSTATUS(status):提取⼦进程退出码 

3.options

默认为0,表示阻塞等待

WNOHANG: 
若指定⼦进程没有结束,返回0
若指定子进程正常结束,返回该⼦进程的PID

**实验:**使用waitpid进程等待一个正常退出的子进程

  • proc.c

实验现象

**实验:**使用waitpid进程等待一个没有创建的子进程

  • proc.c

实验现象

3.3.退出状态(status)

**status:**一个整型变量,描述子进程的退出状态,高16位无效,只研究低16位

正常退出时:

  • **[15,8]:**退出代码
  • [7,0]:0

发生异常时:

  • **[15,8]:**垃圾值
  • **[7]:**core dump标志位(TODO)
  • **[6,0]:**退出信号

**实验:**获取子进程的退出状态

  • proc.c

实验现象

status打印的是256,而不是1

需要将statu右移8位,再打印

  • proc.c

实验现象

3.4.退出信号与异常终止

信号查看

**左侧:**信号值

**右侧:**信号名称

信号的本质是宏,查看的是宏值与宏名称

进程没有异常 时,status的低7个bit位为0,高8位为退出码

进程异常终止 时,status的低7个bit位为异常对应的信号,高8位无意义

**实验:**获取进程没有异常时的退出码与退出信号

  • proc.c

实验现象

**实验:**获取进程异常退出时的退出信号

  • proc.c

实验现象

退出码无意义,退出信号为11,代表段错误,表示有野指针问题

原理图

**实验:**通过宏函数获取退出状态中的退出码数据

  • proc.c

实验现象

**实验:**通过宏函数判断是否发生异常

  • proc.c

实验现象

3.5.阻塞与非阻塞等待

**WNOHANG(Wait No Hang):**非阻塞(None Block)

非阻塞轮询(循环完成)

父进程不断访问子进程,子进程未退出,父进程直接挂断,一段时间后再次访问,直到访问成功

  • **返回值大于0:**等待结束,子进程退出
  • **返回值等于0:**调用结束,子进程没有退出
  • **返回值小于0:**等待失败

父进程在等待子进程的间隙中,可以执行自己的程序,使效率更高

阻塞调用

父进程保持访问子进程状态,不挂断,直到子进程退出,访问成功

**实验:**使用非阻塞调用

  • proc.c

实验现象

**实验:**通过非阻塞调用,让父进程在时间间隙中执行自己的任务

  • proc.c

实验现象

当使用sleep指令后,bash会发生阻塞等待,导致后续的指令无法执行

四、进程程序替换

4.1.程序替换原理

将新路径下的程序代码与数据,覆盖原进程 地址空间的代码与数据

进程 == 内核数据结构(PCB) + 代码和数据,此时PCB不变,调整页表

程序替换的过程中,并没有创建新的进程

只是把当前进程的代码和数据,用新的程序的代码和数据覆盖式地进行替换

一旦程序替换成功,就去执行新的代码,原始代码的后半部分已经不存在了

**实验:**使用execl实现程序替换

  • proc.c

实验现象

exec系列的函数只有失败返回值,没有成功返回值

**实验:**打印execl调用失败时的返回值

  • proc.c

实验现象

4.2.替换函数

非系统调用函数,用语言封装了系统调用函数execve

  • **l(list):**表示参数采用列表
  • **v(vector):**表示参数采用数组
  • **p(path):**有p自动搜索环境变量PATH
  • **e(env):**自己维护环境变量

|---------|------|-------|--------------|
| 函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
| execl | 列表 | 不是 | 是 |
| execlp | 列表 | 是 | 是 |
| execle | 列表 | 不是 | 不是,需自己组装环境变量 |
| execv | 数组 | 不是 | 是 |
| execvp | 数组 | 是 | 是 |
| execvpe | 数组 | 是 | 不是,需自己组装环境变量 |

execl函数

**参数1:**路径+程序名(要执行谁)

**参数2:**命令行怎么写就怎么传(怎么执行)

**最后一个参数:**NULL(表示参数传递完成)

**实验:**创建子进程来调用替换函数

  • proc.c

实验现象

进程具有独立性,子进程的数据和代码都会发送写时拷贝,所以不会影响父进程

程序加载的本质是加载器动态创建进程的过程,exec系列的接口属于加载器范畴

**实验:**使用替换函数替换自己写的程序

  • proc.c

实验现象

**实验:**使用替换函数替换python程序

  • proc.c

实验现象

**实验:**使用替换函数替换shell程序

  • proc.c

实验现象

**实验:**观察发生进程程序替换的子进程PID是否变化

  • proc.c

实验现象

execlp函数

**参数1:**执行的文件名(会在环境变量PATH中查找指定的命令)

**参数2:**命令行怎么写就怎么传(怎么执行)

**最后一个参数:**NULL(表示参数传递完成)

  • proc.c

实验现象

execle函数

**参数1:**路径+程序名(要执行谁)

**参数2:**命令行怎么写就怎么传(怎么执行)

**参数3:**提供一个环境变量表(指针数组,最后一个元素为NULL)

execv函数

**参数1:**路径+程序名(我要执行谁)

**参数2:**提供一个命令行参数表(指针数组,最后一个元素为NULL)

  • proc.c

实验现象

execvp函数

**参数1:**执行的文件名(会在环境变量PATH中查找指定的命令)

**参数2:**提供一个命令行参数表(指针数组,最后一个元素为NULL)

  • proc.c

实验现象

execvpe函数

**参数1:**执行的文件名(会在环境变量PATH中查找指定的命令)

**参数2:**提供一个命令行参数表(指针数组,最后一个元素为NULL)

**参数3:**提供一个环境变量表(指针数组,最后一个元素为NULL)

  • proc.c

实验现象

发生替换的子进程会使用全新的环境变量列表

而不是子进程从父进程中继承下来的环境列表

4.2.putenv函数

**功能:**哪个进程调用就将环境变量以新增方式添加到该进程中

  • proc.c

实验现象

**实验:**使用environ指针让execvpe函数实现新增环境变量

  • proc.c

实验现象

4.3.execve函数

系统调用函数

替换函数的底层都是调用execve函数

五、自主Shell命令行解释器

5.1.Myshell代码

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

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

//shell定义的全局数据

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

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

//别名映射表
std::unordered_map<std::string,std::string> alias_list;

//for test
char cwd[1024];
char cwdenv[1024];

//last exit code
int lastcode = 0;

//获取用户名
const char *GetUserName()
{
	const char *name = getenv("USER");
	return name == NULL ? "None" : name;
}

//获取主机名
const char *GetHostName()
{
	const char *hostname = getenv("HOSTNAME");
	return hostname == NULL ? "None" : hostname;
}

//获取当前路径
const char *GetPwd()
{
	const char *pwd = getcwd(cwd,sizeof(cwd));
	if(pwd != NULL)
	{
		snprintf(cwdenv, sizeof(cwdenv), "PWD=%s",cwd);
		putenv(cwdenv);
	}
	return pwd == NULL ? "None" : pwd;
}

//获取家目录
const char *GetHome()
{
	const char *home = getenv("HOME");
	return home == NULL ? "" : home;
}

//初始化环境变量
void InitEnv()
{
	extern char **environ;
	memset(g_env, 0, sizeof(g_env));
	g_envs = 0;
	//从父进程shell中获取环境变量
	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*)"HAHA=for_test";
	g_env[g_envs] = NULL;
	//导入环境变量
	for(int i = 0; g_env[i]; i++)
	{
		putenv(g_env[i]);
	}
}

//内建命令

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 == "-")
		{
			//cd -
		}
		else if(where == "~")
		{
			//cd ~
		}
		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;
		}
		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;
}

//获取当前路径的目录名
std::string DirName(const char *pwd)
{
#define SLASH "/"
	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 MakeCommandLine(char cmd_prompt[],int size)
{
	snprintf(cmd_prompt, size, FORMAT, GetUserName(),GetHostName(),DirName(GetPwd()).c_str());	
}

//输出命令行
void PrintCommandPrompt()
{
	char prompt[COMMAND_SIZE];
	MakeCommandLine(prompt, sizeof(prompt));
	printf("%s",prompt);
	fflush(stdout);
}

//输入命令行字符串
bool GetCommandLine(char *out, int size)
{
	char *c = fgets(out, size, stdin);
	if(c == NULL)
	{
		return false;
	}
	out[strlen(out) - 1] = 0;//清理\n
	if(strlen(out) == 0)
	{
		return false;
	}
	return true;
}

//命令行分析
bool CommandParse(char *commandline)
{
#define SEP " "
	g_argc = 0;
	//"ls -a -l" -> "ls" "-a" "-l"
	g_argv[g_argc++] = strtok(commandline,SEP);
	while(g_argv[g_argc++] = strtok(nullptr,SEP));
	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]);
	}
}

//检测并且执行内建命令
bool CheckAndExecBuiltin()
{
	std::string cmd = g_argv[0];
	if(cmd  == "cd")
	{
		Cd();
		return true;
	}
	else if(cmd == "echo")
	{
		Echo();	
		return true;
	}
//	else if(cmd == "export")
//	{
//		Export();
//		return true;
//	}
//	else if(cmd == "alias")
//	{
//		std::string nickname = g_argv[1];
//		alias_list.insert(k,v);
//	}
	return false;
}

//执行命令
int Execute()
{
	//#4:执行命令
	pid_t id = fork();
	if(id == 0)
	{
		//子进程
		execvp(g_argv[0],g_argv);
		exit(1);
	}
	int status = 0;
	//父进程
	pid_t rid = waitpid(id, &status, 0);
	if(rid > 0)
	{
		lastcode = WEXITSTATUS(status);
	}
	return 0;
}

int main()
{
	//shell启动时从系统中获取环境变量
	//myshell环境变量从父进程shell获取
	InitEnv();
	while(true)
	{
		//#1:输出命令行提示符
		PrintCommandPrompt();

		//#2:获取用户输入的命令
		char commandline[COMMAND_SIZE];
		if(!GetCommandLine(commandline,sizeof(commandline)))
		{
			//输入失败后重新输入
			continue;
		}

		//#3:命令行分析
		if(!CommandParse(commandline))
		{
			continue;
		}
		//PrintArgv();
		//printf("echo:%s\n",commandline);
		
		//检测别名

		//#4:检测并处理内建命令
		if(CheckAndExecBuiltin())
		{
			continue;
		}

		//#5:执行命令
		Execute();
	}
	return 0;
}
相关推荐
中屹指纹浏览器1 小时前
2026浏览器缓存指纹持久化溯源机制与多层级缓存隔离优化方案
经验分享·笔记
志栋智能2 小时前
超自动化巡检:实现精细化运维管理的基础
运维·服务器·网络·人工智能·自动化
TOSUN同星2 小时前
同星多工位自动化刷写台架,助力汽车电子高效量产与质量追溯
运维·自动化·汽车
夏日听雨眠2 小时前
LInux(gcc处理器,库文件,动静态库)
linux·运维·服务器
羊群智妍2 小时前
2026 AI搜索优化技术实践:GEO监测工具选型报告
笔记
xingfujie2 小时前
Ubuntu K8s 1.28 kubeadm 高可用集群部署实战
linux·运维·服务器·docker·kubernetes
Tutankaaa2 小时前
从单场到多场并发:知识竞赛平台的弹性扩展能力
服务器·笔记·学习·职场和发展
实心儿儿2 小时前
Linux —— 进程间通信 - 命名管道
linux·运维·服务器
sheeta19982 小时前
LeetCode 每日一题笔记 日期:2026.05.11 题目:2553. 分割数组中数字的数位
笔记·算法·leetcode