Linux是怎样工作的--第三章
第三章:进程管理
文章目录
目录
前言
一、创建进程
二、fork()函数
三、execve()函数
1. const char *pathname
2. char *const argv[]
3. char *const envp[]
四、结束进程
总结
前言
本章将介绍内核提供的创建与删除进程的功能,但是目前我们还不了解第五章关于虚拟内存的内容,据无法理解linux创建与删除进程的机制,因此本章抛弃了虚拟内存,单纯讲述进程的创建与删除,第五章就详细介绍完整的运行机制
提示:以下是本篇文章正文内容,下面案例可供参考
一、创建进程
在Linux中,创建进程有如下两个目的:
- 将同一个程序分成多个进程进行运行(例如,使用Web服务器接收多个请求)
- 创建另一个程序(例如,从bash启动一个新的程序)
为了达成目的,Linux提供了fork()(底层为clone()的系统调用)与execve()函数(底层为execve()的系统调用)
二、fork()函数
要将同一个程序分成多个进程进行处理,只需要使用fork()函数即可,调用fork()函数即可,在嗲用fork()函数,基于发起调用的进程,创建一个新的进程的那个进程称为父进程,被创建的哪个进程为子进程
创建新进程的流程如下:
1.为子进程申请内存空间,并复制父进程的内存到子进程的内存空间
2.父进程与子进程分裂成两个进程,以实现不同的代码,这一点的实现依赖于fork函数分别返回不同的值给父进程与子进程
接下来我们实现以下程序对fork函数一探究竟
#include<unistd.h>//提供 fork()(创建进程)和 getpid()(获取当前进程 ID)等系统调用函数。
#include<stdio.h>//提供 printf() 函数(用于输出信息)。
#include<stdlib.h>//提供 exit() 函数(终止进程)和 EXIT_SUCCESS/EXIT_FAILURE 等状态宏(表示进程退出状态)。
#include<err.h>static void child()//static内部函数的内部工具,不对外提供接口
{printf("I'm child!my pid is %d.\n",getpid());exit(EXIT_SUCCESS);
}
static void parent(pid_t pid_c)
{printf("I'm parent!my pid is %d and the pid of my child is %d.\n",getpid(),pid_c);exit(EXIT_SUCCESS);
}int main()
{pid_t ret;ret=fork();if(ret==-1)//创建子进程失败(如系统资源不足)。{err(EXIT_FAILURE,"fork() failure");}if(ret==0)//表示当前执行的是子进程{child();}else{parent(ret);//正整数:表示当前执行的是父进程}err(EXIT_FAILURE,"shouldn't reach here");
}
当父进程和子进程从fork()函数中恢复时,会获得fork的反沪指,fork对父进程返回子进程的ID,对与子进程返回0,编译并运行上述代码
cc -o fork fork.c
./fork
三、execve()函数
在打算启动另一个程序时,需要调用execve()函数,我们先看一下内核在运行时的流程
- 读取可执行文件,并读取创进程的内存映像所需的信息
- 用新进程的数据覆盖当前进程的内存
- 从最初命令开始运行新进程
也就是说,在启动另一个程序时,并非新增一个进程,而是替换了当前进程
接下来我们详细的说明这一流程,首先,读取可执行文件,以及创建进程的内存映像所需的信息,可执行文件中不仅能包含进程在运行中使用的代码与数据,还包含开始运行程序时所需的数据
- 代码的代码段在文件中的偏移量,大小,以及内存映像的地址
- 代码以外的变量等数据的数据段在文件中的偏移量,大小以及内存映像的起始地址
- 程序执行的第一条指令的内存地址(第一段)
与高级编程语言编写的代码不同,在CPU上执行机器语言必须提供需要的操作的内存地址,因此在代码段和数据段中必须包含内存映像的起始地址,比如使用伪代码编写下一段源代码
c=a+b
在机器语言层面,这段源代码将转变成下面直接对内存地址进行操作的指令
load m100 r0 将内存地址100(变量a)的值读取到名为r0的寄存器中
load m200 r1 将内存地址200(变量b)的值读取到名为r1的寄存器中
add r0 r1 r2 将r0与r1相加,并将结果存在r2的寄存器中
store r2 m300 将r2的值纯存在内存m300中
最后从入口执行程序
然而,Linux可执行文件的结构遵循名字为ELF(excutable and linkable format,可执行与可连接结构),ELF的相关信息可通过readelf命令来获取
西面我们可以尝试获取/bin/sleep的ELF信息
通过附件-h选项,可以获取起始地址
通过附加-S选项,可以获取代码段与数据段在文件中的偏移量,大小和起始地址
在程序运行时创建的进程的内存映像信息,可以从/proc/pid/maps这一文件中找到,sleep命令的相关信息如下
再打算新建一个别的进程时,通常采用称为fork and exec的方式,既有父进程调用fork()创建子进程,再由子进程调用exec(),下图所示为bash进程创建echo进程的流程
#include<unistd.h>//提供系统调用接口,如fork()(创建子进程)、execve()(执行新程序)、getpid()(获取当前进程 PID)、fflush()(刷新缓冲区)。
#include<stdio.h>//提供标准输入输出函数,如printf()。
#include<stdlib.h>//提供程序退出状态定义,如EXIT_SUCCESS(成功退出,值为 0)、EXIT_FAILURE(失败退出,值为非 0)。
#include<err.h>//提供err()函数,用于简化错误处理(打印错误信息并退出程序)。
static void child() {char* args[] = {"/bin/echo", "hello", NULL}; // 传给新程序的参数列表printf("I'm child! my pid is %d.\n", getpid()); // 打印子进程PIDfflush(stdout); // 强制刷新输出缓冲区execve("/bin/echo", args, NULL); // 执行新程序/bin/echoerr(EXIT_FAILURE, "exec() failed"); // 如果execve失败,打印错误并退出
}
static void parent(pid_t pid_c) {printf("I'm parent! my pid is %d and the pid of my child is %d.\n", getpid(), pid_c);exit(EXIT_SUCCESS); // 父进程正常退出
}
int main() {pid_t ret;ret = fork(); // 创建子进程if (ret == -1) { // fork失败的情况err(EXIT_FAILURE, "fork() failure");}if (ret == 0) { // 当前是子进程child();} else { // 当前是父进程(ret为子进程PID)parent(ret);}err(EXIT_FAILURE, "shouldn't reach here"); // 理论上不会执行
}
- 程序启动,
main()
函数调用fork()
创建子进程。 - 若
fork()
失败(ret == -1
),调用err()
打印错误并退出。 - 若
ret == 0
(当前是子进程):- 调用
child()
函数,打印子进程 PID。 - 执行
execve("/bin/echo", ...)
,子进程被替换为/bin/echo
程序,输出 "hello" 后退出。
- 调用
- 若
ret > 0
(当前是父进程,ret
为子进程 PID):- 调用
parent(ret)
函数,打印父进程 PID 和子进程 PID。 - 调用
exit(EXIT_SUCCESS)
正常退出。
- 调用
execve
是 Unix/Linux 系统中一个核心的系统调用,用于在当前进程中执行一个新的程序。它的作用是将当前进程的代码、数据、堆栈等全部替换为新程序的内容(进程 ID 保持不变),因此也被称为 “进程替换”
#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);
1. const char *pathname
- 作用:指定要执行的新程序的路径(可以是绝对路径或相对路径)。
- 要求:必须是一个可执行文件(二进制文件或脚本文件,脚本文件需在首行指定解释器,如
#!/bin/bash
),且当前进程对该文件有可执行权限(x
权限)。 - 示例:
"/bin/echo"
(绝对路径)、"./myprogram"
(相对路径,当前目录下的可执行文件)。
2. char *const argv[]
- 作用:传递给新程序的命令行参数数组。
- 格式要求:
- 数组中的每个元素是一个字符串(参数),按顺序对应新程序的命令行参数。
- 惯例上,
argv[0]
是新程序的名称(通常与pathname
的文件名一致,也可自定义)。 - 数组必须以
NULL
结尾(标记参数列表结束)。
- 示例:在之前的代码中,
char* args[] = {"/bin/echo", "hello", NULL};
表示给/bin/echo
程序传递参数hello
,等价于命令行执行echo hello
。
3. char *const envp[]
- 作用:传递给新程序的环境变量数组。
- 格式要求:
- 数组中的每个元素是一个 “键 = 值” 格式的字符串(如
"PATH=/usr/bin"
、"USER=root"
)。 - 数组必须以
NULL
结尾。
- 数组中的每个元素是一个 “键 = 值” 格式的字符串(如
- 特殊情况:
- 如果
envp
为NULL
,新程序会继承当前进程的环境变量(通过全局变量environ
获取)。 - 若需自定义环境变量,需显式构造该数组(例如
char* env[] = {"LANG=en_US", "DEBUG=1", NULL};
)。
- 如果
编译上述代码
四、结束进程
可以调用_exit()函数(底层发起exit_group()系统调用),来结束进程,进程结束后,所有分配给进程的内存将被回收
但是我们很少会使用_exit()函数,而是通过调用C标准库的exit()函数来结束进程的运行,在这种情况下C标准库会在调用自身的种植处理后调用_exit()函数
总结
本文主要时学习了进程的创建以及结束