【Linux】进程控制和Shell的简易实现
1.进程创建
fork函数
pid_t fork()
函数就从已存在进程中创建一个进程,新进程为子进程,而原进程就为父进程。
头文件:#include <sys/types.h>
#include <unistd.h>
返回值:子进程就返回0
,父进程返回当前子进程id
,出错返回-1
进程调用 fork ,当控制块转移到内核中 fork 代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程的部分数据结构内容拷贝给子进程
- 添加子进程到进程的系统进程
- fork返回,父子进程分别从 fork() 之后的代码开始执行,调度器开始调度进程
写时拷贝
通常,父子代码是共享的,父子不再写入时候,数据也是共享的,进程共享父进程的内存空间,但内核会标记为 只读 ,当任意一方试图写入,内核会为进程复制一份私有副本,防止进程之间相互影响
因为有了写时拷贝技术的存在,父子进程就可以数据就可以·分离开来,保证了进程之间的独立性
写时拷贝,是一种延时申请内存的技术,可以提高内存的利用率
fork常规用法
- 父进程创建子进程,父子进程执行不同的代码段,例如:父进程等待客户端请求,子进程来处理请求或者用子进程来执行另一个不同的程序,子进程通过exec()进程替换来执行另一个任务
fork调用失败原因
- 系统中有太多进程,而进程条目的数量是有限的,就会导致创建子进程失败
- 实际用户的进程数受到了限制
fork()
行为总结
行为 | 父进程 | 子进程 |
---|---|---|
fork() 返回值 | 子进程 PID(>0) | 0 |
执行起点 | fork() 后的下一行代码 | fork() 后的下一行代码 |
内存修改 | 触发写时拷贝(COW) | 触发写时拷贝(COW) |
典型用途 | 管理子进程 | 执行新任务 |
(注意:父子进程两个执行流谁先执行完取决于调度器)
2.进程终止
进程终止的本质是释放系统资源,就是释放进程申请的相关内核结构和对应的代码和数据
进程退出场景
- 代码运行完毕,正确退出(从main返回,return 0)
- 代码运行完毕,异常退出 (exit(1))
- 代码异常终止·(ctrl + c)
退出码
我们通过 退出码 得到最近一次执行程序的退出状态,来了解程序是否正常退出
Linux 退出码:
我们可以通过strerror
函数来查看对应退出码的描述
进程退出码查看
Linux 进程退出我们可以通过echo $?
来查看进程退出码。
比如我正确执行一条指令,它的退出码就为0(Linux中执行指令都是通过bash创建子进程来执行的)
进程退出方法
- _exit函数
_exit 函数是系统调用函数
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值
- exit函数
exit最后也会调⽤_exit, 但在调⽤_exit之前,还做了其他工作:
- 执行用户通过
atexit
或on_exit
定义的清理函数。
#include <stdlib.h>
int on_exit(void (*function)(int , void *), void *arg);
#include <stdio.h>
#include <stdlib.h>
void cleanup(int status, void *msg) {
printf("Exit status: %d, Message: %s\n", status, (char *)msg);
}
int main() {
on_exit(cleanup, (void *)"Bye!"); // 注册带参数的清理函数
printf("Main function\n");
exit(42); // 退出状态 42
}
输出:
Main function
Exit status: 42, Message: Bye!
- 关闭所有打开的流,所有缓存数据均被写入
- 调用_exit
exit和_exit函数的区别:exit是对_exit系统调用封装的库函数,它就可以刷新用户缓冲区数据,也就是fflush函数刷新用户缓冲区数据到内核缓冲区中,并且还会调用atexit和on_exit清理函数
int main()
{
printf("hello");
exit(0);
}
运⾏结果:
[root@localhost linux]# ./a.out
hello[root@localhost linux]#
int main()
{
printf("hello");
_exit(0);
}
运⾏结果:
[root@localhost linux]# ./a.out
[root@localhost linux]#
- return返回
return 是一种常见的退出进程方法,return n 等同于执行 exit(n),因为调用 main 的运行函数会将 main 的返回值当 exit 的参数。
3.进程等待
僵尸进程
僵尸进程就是已经终止但未被父进程回收的进程。
进程等待
如果想要对僵尸进程进行处理,就需要父进程父进程回收子进程的方式来解决子进程的僵尸状态。
进程等待方法
wait
- 函数方法:
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
参数: status:保存子进程退出状态(需用宏解析,如 WEXITSTATUS)。
返回值: 成功:返回终止的子进程 PID。
失败:返回 -1(如无子进程)。
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid = fork();
if (pid == 0)
{
// 子进程
printf("Child PID: %d\n", getpid());
_exit(42); // 子进程退出码 42
}
else
{
// 父进程
int status;
pid_t child_pid = wait(&status); // 阻塞等待
printf("Child %d exited with status: %d\n",
child_pid, WEXITSTATUS(status));
}
return 0;
}
输出:
Child PID: 1234
Child 1234 exited with status: 42
waitpid
- 函数方法:
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的⼦进程的进程ID;
如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;
如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;
参数:
pid:
Pid=-1: 等待任⼀个⼦进程。与wait等效。
Pid>0: 等待其进程ID与pid相等的⼦进程。
status: 输出型参数
WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)
options: 默认为0,表⽰阻塞等待
WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等
待。若正常结束,则返回该⼦进程的ID。
等待子进程:
pid_t child_pid = fork();
if (child_pid == 0)
{
// 子进程
_exit(10);
}
else
{
int status;
// 只等待 child_pid 的子进程
waitpid(child_pid, &status, 0);
printf("Child %d exited: %d\n", child_pid, WEXITSTATUS(status));
}
非阻塞轮询:
int status;
pid_t pid = waitpid(-1, &status, WNOHANG);
if (pid > 0)
{
printf("Child %d exited.\n", pid);
}
else if (pid == 0)
{
printf("No child exited yet.\n");
}
else
{
perror("waitpid failed");
}
获取子进程status
-
wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。
-
如果传递NULL,表⽰不关心⼦进程的退出状态信息。
-
否则,操作系统会根据该参数,将子进程的退出信息反馈给传递父进程
status 可以当做一个位图来看,其具体实现细节如下图:
退出状态和终止信号以及core dump表示方式
1.正常终止
退出状态:(status >> 8)&0xFF
2. 被信号所杀
退出信号:status&0x7F
core dump(核心转储):(status>>7)&1
进程替换
替换原理
用 fork 创建的子进程和父进程执行的是同一个程序,如果子进程想要执行另一个程序,往往需要调用 exec 函数来替换当前程序。当进程调用 exec 程序后,进程代码和数据就完全被新进程替换,从新程序的启动例程开始执行,并且执行完新进程后并不会返回之前子进程执行的代码继续执行,而是直接退出,并且调用exec并不会创建新进程,所以调用exec前后该进程的id并没有改变
返回值处理: 所有函数调用成功时不返回,失败时返回-1并设置errno。
替换函数
其中有六个exec开头的函数,统称exec函数:
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
函数名 | 参数传递方式 | 路径处理 | 环境变量处理 | 是否系统调用 | 其他特点 |
---|---|---|---|---|---|
execl | 可变参数列表 | 需完整路径(如/bin/ls ) | 继承当前环境变量 | 否 | 参数以 NULL 结尾(如 "ls", "-l", NULL |
execlp | 可变参数列表 | 从 PATH 搜索文件名 | 继承当前环境变量 | 否 | 自动搜索可执行文件路径(如 execlp("ls", ...) ) |
execle | 可变参数列表 | 需完整路径 | 自定义 envp 数组 | 否 | 参数列表后需显式传递 envp (如 execle(..., envp) )) |
execv | 参数数组 argv[] | 需完整路径 | 继承当前环境变量 | 否 | 参数数组需以 NULL 结尾(如 char *argv[] = {"ls", "-l", NULL} )) |
execvp | 参数数组 argv[] | 从 PATH 搜索文件名 | 继承当前环境变量 | 否 | 结合路径搜索与数组传参(如 execvp("ls", argv) ) |
execve | 参数数组 argv[] | 需完整路径 | 自定义 envp 数组 | 是 | 唯一直接调用内核的系统调用(其他函数均封装它) |
使用方法:
- execl
特点:参数列表形式传参、需完整路径、继承环境变量
#include <unistd.h>
#include <stdio.h>
int main() {
printf("execl调用示例\n");
// 执行/bin/ls,参数列表需以NULL结尾
if (execl("/bin/ls", "ls", "-l", NULL) == -1) {
perror("execl失败");
}
return 0;
}
- execlp
特点:参数列表传参、自动搜索PATH环境变量
#include <unistd.h>
#include <stdio.h>
int main() {
printf("execlp调用示例\n");
// 自动搜索PATH中的"ls"可执行文件
if (execlp("ls", "ls", "-l", NULL) == -1) {
perror("execlp失败");
}
return 0;
}
- execle
特点:参数列表传参、需完整路径、自定义环境变量
#include <unistd.h>
#include <stdio.h>
int main() {
char *envp[] = {"CUSTOM_ENV=test", "PATH=/bin", NULL};
printf("execle调用示例\n");
// 传递自定义环境变量envp
if (execle("/bin/ls", "ls", "-l", NULL, envp) == -1) {
perror("execle失败");
}
return 0;
}
- execv
特点:参数数组传参、需完整路径、继承环境变量
#include <unistd.h>
#include <stdio.h>
int main() {
char *argv[] = {"ls", "-l", NULL};
printf("execv调用示例\n");
if (execv("/bin/ls", argv) == -1) {
perror("execv失败");
}
return 0;
}
- execvp
特点:参数数组传参、自动搜索PATH环境变量
#include <unistd.h>
#include <stdio.h>
int main() {
char *argv[] = {"ls", "-l", NULL};
printf("execvp调用示例\n");
if (execvp("ls", argv) == -1) {
perror("execvp失败");
}
return 0;
}
- execve
特点:参数数组传参、需完整路径、自定义环境变量、唯一系统调用
#include <unistd.h>
#include <stdio.h>
int main() {
char *argv[] = {"ls", "-l", NULL};
char *envp[] = {"CUSTOM_ENV=test", "PATH=/bin", NULL};
printf("execve调用示例\n");
if (execve("/bin/ls", argv, envp) == -1) {
perror("execve失败");
}
return 0;
}
总结
总结下来就是 是否需要完整路径、自定义环境变量,以及参数是列表还是数组
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p⾃动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
4,简单shell的实现
**前言:**我们在命令行执行命令时都是由 bash 创建子进程,然后由子进程 exec进程替换 执行对应命令。
shell脚本的流程:
- 获取命令行
- 解析命令行
- fork()创建子进程
- 替换子进程
- 父进程等待子进程退出
源码实现:
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
using namespace std;
const int basesize = 1024;
const int gnum = 64;//环境变量表和命令行参数表的大小
char* genv[gnum];
char* gargv[gnum];
char buff[basesize];//辅助数组,存储输入的命令
char pwd[basesize];
char pwdenv[basesize];
int gargc = 0;
int lastcode = 0;
string getUsername()
{
string username = getenv("USER");
return username == "" ? "None" : username;
}
string getHostname()
{
char hostname[20];
int n = gethostname(hostname,sizeof(hostname));
if(n < 0)
{
perror("gethostname");
exit(1);
}
return hostname;
}
string GetPwd()
{
if(getcwd(pwd,sizeof(pwd)) == nullptr)
{
return "Node";
}
snprintf(pwdenv,sizeof(pwdenv),"PWD=%s",pwd);
for(int i = 0;genv[i];i++)
{
string str = genv[i];
char* before = (char*)str.substr(0,3).c_str();
if(strcmp(before,"PWD") == 0)
{
strncpy(genv[i],pwdenv,strlen(pwdenv));
genv[i][strlen(pwdenv)] = 0;
return pwd;
}
}
}
string Lastdir()
{
string cur = GetPwd();
if(cur == "/" || cur == "None")
return cur;
size_t pos = cur.rfind('/');
if(pos == std::string::npos) return cur;
return cur.substr(pos + 1);
}
void PrintCommand()
{
string username = getUsername();
string hostname = getHostname();
string pwd = Lastdir();
if(username != "None" && hostname != "None" && pwd != "None")
{
cout << username << "@" << hostname << ":" << pwd << "$";
fflush(stdout);
}
else
exit(1);
}
bool GetCommand()
{
memset(buff,0,sizeof buff);
char* result = fgets(buff,basesize,stdin);
if(result == nullptr)
{
return false;
}
//cout << result << endl;
buff[strlen(buff) - 1] = 0;
if(strlen(buff) == 0) return false;
return true;
}
void ParseCommand()
{
const char* sep = " ";
gargc = 0;
gargv[gargc++] = strtok(buff,sep);
while((bool)(gargv[gargc++] = strtok(nullptr,sep)));
//cout << gargc << endl;
gargv[gargc] = nullptr;
gargc--;
}
bool ExecuteCommand()
{
pid_t id = fork();
if(id < 0) return false;
if(id == 0)
{
//子进程
// 1. 执行命令
cout << "gragvp[0] = " << gargv[0] << endl;
int ret = execvpe(gargv[0], gargv, genv);
//char* argv[] = {
// "ls",
// "-l",
// "-a",
// nullptr
// };
//int ret = execvpe("ls",gargv,genv);
cout << errno << endl;
lastcode = 1;
// 2. 退出
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
return true;
}
lastcode = 100;
return false;
//pid_t id = fork();
//if(id == 0)
//{
// cout << gargv[0] << endl;
// int ret = execvpe(gargv[0],gargv,genv);
// if(ret == -1)
// {
// cout << errno << endl;
// return false;
// }
// return true;
//}
//int status;
//pid_t wid = waitpid(id,&status,0);
//if(wid > 0)
//{
// cout << "等待子进程成功" << endl;
// return true;
//}
//return false;
}
void AddEnv()
{
int index = 0;
while(genv[index])
{
index++;
}
genv[index] = (char*)malloc(strlen(gargv[1] + 1));
strncpy(genv[index],gargv[1],strlen(gargv[1]) + 1);
genv[++index] = nullptr;
}
bool CheckCommand()
{
if(strcmp(gargv[0],"cd") == 0)
{
if(gargc == 2)
{
chdir(gargv[1]);
GetPwd();
lastcode = 0;
}
else
{
lastcode = 2;
}
return true;
}
else if(strcmp(gargv[0],"echo") == 0)
{
if(gargc == 2)
{
if(gargv[1][0] == '$')
{
if(gargv[1][1] == '?')
{
printf("%d\n",lastcode);
lastcode = 0;
}
}
else
{
printf("%s\n",gargv[1]);
lastcode = 0;
}
}
else
lastcode = 3;
return true;
}
else if(strcmp(gargv[0],"env") == 0)
{
for(int i = 0;genv[i];i++)
cout << genv[i] << endl;
lastcode = 0;
return true;
}
else if(strcmp(gargv[0],"export") == 0)
{
if(gargc == 2)
{
AddEnv();
lastcode = 0;
return true;
}
else
lastcode = 4;
return true;
}
return false;
}
void debug()
{
for(int i = 0;genv[i];i++)
{
cout << genv[i] << endl;
}
cout << "//" << endl;
for(int i = 0;gargv[i];i++)
{
cout << gargv[i] << endl;
}
}
void Initenv()
{
extern char** environ;
int index = 0;
while(environ[index])
{
genv[index] = (char*)malloc(strlen(environ[index] + 1));
strncpy(genv[index],environ[index],strlen(environ[index]));
index++;
}
genv[index] = nullptr;
}
int main()
{
Initenv();
while(1)
{
PrintCommand();//打印命令提示符
//sleep(10);
bool ret = GetCommand();//从标准输入获取命令
if(ret = true)
ParseCommand();//分析命令
else
continue;
// cout << endl;
// debug();
if(CheckCommand())
{
continue;
}
ExecuteCommand();//处理命令
}
return 0;
}