【Linux】进程控制

1. 进程创建

1.1 fork函数

cs 复制代码
#include <unistd.h>
pid_t fork(void);

进程调用fork,当控制转移到内核中的fork代码后,内核做:

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

1.2 fork返回值

  • 子进程返回0
  • 父进程返回的是子进程的pid
  • 创建出错返回-1

1.3 写时拷贝

刚创建时,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本:

1.4 fork调失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

2. 进程终止

本质是释放系统资源

2.1 进程退出场景

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

2.2 进程常见退出方法

正常终止(可以通过 echo $? 查看进程退出码):

  1. 从main返回
  2. 调用exit
  3. 调用_exit

异常退出: ctrl + c,信号终止

2.2.1 退出码

程序返回退出代码0 时表示执行成功没有问题。 !0被视为不成功

2.2.2 _exit函数

cs 复制代码
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值

说明:虽然status是int,但是仅有低8位可以被⽗进程所⽤。所以_exit(-1)时,在终端执行$?发现 返回值是255

2.2.3 exit函数

cs 复制代码
#include <unistd.h>
void exit(int status);

exit最后也会调用_exit,但在调用_exit之前,还做了其他工作:

  1. 执行用户通过atexit或on_exit定义的清理函数
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

3. 进程等待

3.1 进程等待必要性

  • 子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

3.2 进程等待的方法

1)wait

cs 复制代码
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
 成功返回被等待进程pid,失败返回-1。
参数:
 输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL

2)waitpid

cs 复制代码
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
  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会⽴即返回,并且释放资源,获得子进程退出信息
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞
  • 如果不存在该子进程,则立即出错返回

3)获取子进程status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
  • 如果传递NULL,表示不关心子进程的退出状态信息
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16bit位):

4)阻塞与非阻塞等待

cs 复制代码
// 阻塞模式(第三个参数=0)
waitpid(-1, &status, 0); 

// 非阻塞模式(第三个参数=WNOHANG)
waitpid(-1, &status, WNOHANG);
1. 阻塞模式
  • 行为 :调用 waitpid 后,父进程会暂停所有操作(挂起),不再执行后续代码,直到「子进程退出」这个事件发生。
  • 核心特点:等待期间,调用者(父进程)"啥也干不了",CPU 不会给它分配时间片(节省 CPU 资源)
2. 非阻塞模式(时时询问)
  • 行为 :调用 waitpid 后,父进程不会暂停 ,而是立即返回结果:
    • 如果子进程还在运行(事件未发生),返回 0
    • 如果子进程已退出(事件发生),返回子进程 PID;
    • 失败返回 -1
  • 核心特点:等待期间,调用者(父进程)"不闲着",可以执行其他任务,但需要主动循环查询结果(轮询),可能消耗更多 CPU。

4. 进程程序替换

fork 创建的子进程只是执行父进程代码的 "不同分支",但本质上子进程和父进程跑的是同一个程序文件(a.out)。但实际开发中,子进程往往需要执行 另一个完全不同的程序,所以进程的程序替换来完成这个功能

4.1 替换原理

1. 「替换」的是用户空间,不是进程本身
  • 子进程调用 exec 前:和父进程共享同一个程序文件(a.out),用户空间是父进程的副本(代码、数据都一样);
  • 子进程调用 exec 后:用户空间的所有内容被新程序(比如/bin/ls)替换 ------ 原来的 a.out 代码、数据全没了,换成新程序的代码和数据,执行入口变成新程序的启动例程(最终调用新程序的main)。
2. 「不变」的是内核层面的进程属性

调用 exec 不会创建新进程(所以 PID 不变),进程的核心内核数据结构(task_struct)没被重建,只是更新了用户空间的映射。比如:

  • PID、PPID(父进程 ID)不变;
  • 已经打开的文件描述符(比如标准输入、输出)不变;
  • 进程的工作目录、用户 ID、组 ID 不变。

4.2 替换函数

有六种 **exec***开头的函数

函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值
  • 原来的程序会被新程序完全覆盖了,换成了新程序的代码和数据

命名解释

后缀 含义
l list(列表传参):参数以可变参数列表形式传递,必须以 NULL 结尾
v vector(数组传参):参数以字符串数组形式传递,数组最后一个元素必须是 NULL
p path(路径查找):自动从系统 PATH 环境变量中查找程序,无需写全路径
e environment(自定义环境):允许传入自定义环境变量数组,覆盖默认环境
cs 复制代码
#include <unistd.h>
//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[]);
1. execl ------ 列表传参 + 需全路径
cs 复制代码
int execl(const char *path, const char *arg, ...);

execl("/bin/ls", "ls", "-l", NULL); // 正确:最后一个参数是NULL
// execl("/bin/ls", "-l", NULL); // 错误:第一个参数必须是程序名(ls)
2. execlp ------ 列表传参 + PATH 查找(最常用之一)
cs 复制代码
int execlp(const char *file, const char *arg, ...);

execlp("ls", "ls", "-l", NULL); // 正确:自动从PATH找ls程序
execlp("pwd", "pwd", NULL);     // 正确:执行pwd命令,无额外参数
3. execv ------ 数组传参 + 需全路径
cs 复制代码
int execv(const char *path, char *const argv[]);

char *argv[] = {"ls", "-l", "-a", NULL}; // 数组结尾必须是NULL
execv("/bin/ls", argv);
4. execvp ------ 数组传参 + PATH 查找(最常用之一)
cs 复制代码
int execvp(const char *file, char *const argv[]);

char *argv[] = {"ps", "aux", NULL};
execvp("ps", argv); // 自动从PATH找ps程序
5. execle ------ 列表传参 + 自定义环境
cs 复制代码
int execle(const char *path, const char *arg, ..., char *const envp[]);

char *envp[] = {"MY_ENV=test", NULL};
// 执行 sh -c "echo $MY_ENV":通过Shell解析环境变量
execle("/bin/sh", "sh", "-c", "echo $MY_ENV", NULL, envp);
6. execvpe ------ 数组传参 + PATH 查找 + 自定义环境
cs 复制代码
int execvpe(const char *file, char *const argv[], char *const envp[]);

char *argv[] = {"sh", "-c", "echo $MY_ENV", NULL};
char *envp[] = {"MY_ENV=test", NULL};
execvpe("sh", argv, envp); // 自动从PATH找sh程序

5. 自主Shell命令行解释器

所以要写⼀个shell,需要循环以下过程:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)

1.从环境变量里获得UESR,HOSTNAME,PWD,模拟shell打印输出行结果

2.

bash 复制代码
1 #include <iostream>
  2 #include <cstdio>
  3 #include <cstring>
  4 #include <cstdlib>
  5 
  6 #define COMMAND_SIZE 1024
  7 #define FORMAT "[%s@%s %s]#"
  8  
  9 const char *GetUserName()
 10 {
 11   const char *name = getenv("USER");
 12   return name == NULL ? "None" : name;
 13 }
 14 
 15 const char *GetHostName()
 16 {
 17   const char *hostname = getenv("HOSTNAME");
 18   return hostname == NULL ? "None" : hostname;
 19 }
 20 
 21 const char *GetPwd()
 22 {
 23   const char *pwd = getenv("PWD");
 24   return pwd == NULL ? "None" : pwd;
 25 }
 26 
 27 void MakeCommandLine(char cmd_prompt[],int size)
 28 {
 29   //格式化命令行提示符
 30   snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),GetPwd());
 31 }
 32 
 33 void PrintCommandPrompt()
 34 {                                                                                                                                                                               
 35   //打印命令行提示符
 36   char prompt [COMMAND_SIZE];
 37   MakeCommandLine(prompt,sizeof(prompt));
 38   printf("%s",prompt);
 39   fflush(stdout);//刷新
 40 }
 41 
 42 bool GetCommandLine(char *out,int size)
 43 {
 44 
 45     char *c = fgets(out,size,stdin);
 46     if(c == NULL) return false;
 47     //echo ls -a -l\n 
 48     out[strlen(out)-1]=0;//清理输入最后的回车键
 49     if(strlen(out) == 0) return false;//用户什么都不输入情况
 50     return true;
 51 }
 52 
 53 int main()
 54 {
 55   while(1)
 56   {
 57       // 1. 输出命令行提示符
 58        PrintCommandPrompt();
 59       // 2. 获取用户输入命令
 60       char commandline[COMMAND_SIZE];
 61       if(! GetCommandLine(commandline,sizeof(commandline)))
 62        continue;  
 63       printf("%s\n",commandline);
 64   }
 65   return 0;
 66 }

3.开始分割输入字符串,创建argv表

strtok 是 C 语言标准库(string.h)中的一个字符串分割函数,用于按照指定的分隔符将一个字符串拆分成多个子串

cpp 复制代码
#include <string.h>
char *strtok(char *str, const char *delim);
  • str :首次调用时,传入要分割的原始字符串;后续调用时,传入 NULL:表示 "继续处理上一个字符串"
  • delim :一个字符串,包含所有用作分隔符的字符(例如 ",; " 表示逗号、分号、空格都是分隔符)。

chdir

chdir 函数接收一个以空字符('\0')结尾的 C 风格字符串(const char*,用于指定要切换到的目标目录路径

因为先改变路径,再更新环境变量,如果一直用getenv(),则在实现内建命令cd时输出命令行的环境地址不能及时改变,所以需要使用系统调用getcwd()

复制代码
char *getcwd(char *buf, size_t size);
  • buf:指向一个字符数组的指针,用于存储获取到的当前工作目录路径。
  • size:指定 buf 的大小(以字节为单位)。

但此时环境变量中pwd还没有改变,仍需修改:把当前环境变量putenv导给环境变量

增加环境变量表:

最终代码

myshell(无内建命令版

完整myshell

相关推荐
视觉装置在笑7139 小时前
Shell 变量基础与进阶知识
linux·运维
阿正的梦工坊9 小时前
DreamGym:通过经验合成实现代理学习的可扩展化
人工智能·算法·大模型·llm
小武~9 小时前
Leetcode 每日一题C 语言版 -- 45 jump game ii
c语言·算法·leetcode
Web极客码9 小时前
如何通过命令行工具检查 Linux 版本信息
linux·运维·服务器
行云流水6269 小时前
前端树形结构实现勾选,半勾选,取消勾选。
前端·算法
欢鸽儿9 小时前
Vitis】Linux 下彻底清除启动界面 Recent Workspaces 历史路径
linux·嵌入式硬件·fpga
繁华似锦respect10 小时前
C++ 智能指针底层实现深度解析
linux·开发语言·c++·设计模式·代理模式
远程软件小帮手10 小时前
云电脑挂机功能上线!边上班摸鱼边游戏多开教程
运维·服务器
laocooon52385788610 小时前
一个C项目实现框架
c语言·算法