虚拟地址空间
虚拟地址空间将程序和物理内存隔离开,程序中访问的内存地址不是实际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到适当的物理内存地址上。
- 内核区
- 内核空间不允许应用程序读写
- 内核驻留在内存中
- 系统中所有进程对应的虚拟地址空间的内核区都会映射到同一块物理内存上
- 用户区
- (1)保留区: 未赋予物理地址,任何对它的引用都是非法的,程序中的空指针指向的内存地址。
- (2).text段: 代码段,存放程序的执行代码 (CPU执行的机器指令),代码段是只读的。
- (3).data段: 数据段,存放程序中已初始化且初值不为0的全局变量和静态变量。
- (4).bss段: 存放程序中未初始化以及初始为0的全局变量和静态变量,操作系统把这些未初始化的变量初始化为0。
- (5)堆区:用于存放进程运行时动态分配的内存。由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。堆向高地址扩展,由于系统用链表来存储空闲内存地址,是不连续的内存区域,堆中内容只能通过指针间接访问。
- (6)内存映射区:磁盘文件映射或程序运行过程中需要调用的动态库。
- (7)栈区: 存储函数内部的非静态局部变量、函数参数等信息,栈内存由编译器自动分配释放。栈向低地址扩展,分配的内存是连续的。
- (8)命令行参数:存储进程执行时传递给main函数的参数。
- (9)环境变量: 存储和进程相关的环境变量,比如:工作路径,进程所有者等信息。
exec族系统调用
exec族系统调用用于执行一个可执行程序,执行成功后不会返回,因为执行该系统调用的进程,虚拟内存空间的用户区(包括代码段、数据段、堆、栈等)都被新内容替代。我们一般在子进程中调用 exec 族函数,子进程的用户区被替换掉开始执行新程序中的代码逻辑,而父进程不受任何影响仍然可以继续正常工作。
int execl(const char* path, const char* arg, ...);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
#if 0
execl("/bin/ps", "title", "aux", NULL);
#else
execlp("ps", "title", "aux", NULL);
#endif
perror("execlp");
}
else if (pid > 0)
{
printf("我是父进程。\n");
sleep(1);
}
return 0;
}
进程创建
fork函数调用成功之后,会返回两个值,父子进程的返回值是不同的。
- 父进程的虚拟地址空间中将该返回值标记为一个大于 0 的数,记录的是子进程的进程 ID
- 子进程的虚拟地址空间中将该返回值标记 0
进程执行位置
- 父进程从main函数开始运行
- 子进程是在父进程中调用fork函数之后被创建,子进程就从 fork函数之后开始向下执行代码
int main()
{
pid_t pid = fork();
printf("父进程返回值: %d\n", pid);
if(pid > 0)
{
printf("我是父进程, pid = %d\n", getpid());
}
else if(pid == 0)
{
printf("我是子进程, pid = %d, 我爹是: %d\n", getpid(), getppid());
}
else
{
}
return 0;
}
写时复制技术
子进程的虚拟地址空间基于父进程的虚拟地址空间拷贝,父子进程用户区数据(代码区、全局数据区、堆区、内存映射区、栈区、环境变量、文件描述符表等)是相同的。假如创建子进程就进行内存拷贝,而之后执行exec族函数,使原进程虚拟内存空间的用户区被新内容替代,处于效率考虑,Linux中引入“写时复制技术”。 在执行fork系统调用之后,在执行exec族系统调用之前,父子进程用的是相同的内存空间,子进程只建立页表映射关系,并不进行内存拷贝,也就是说,父子进程的虚拟地址空间不同,但其对应的内存空间是同一个。当父子进程中任何一个进程试图修改虚拟地址空间里的内容时,如果不是因为exec族系统调用,内核会给子进程的数据段、堆区、栈区等分配相应的物理空间,至此两者有各自的进程空间,而代码段继续共享父进程的物理空间。如果是因为exec族系统调用,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
循环创建子进程问题
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
for (int i = 0; i < 3; ++i)
{
pid_t pid = fork();
printf("当前进程pid: %d\n", getpid());
}
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid;
for (int i = 0; i < 3; ++i)
{
pid = fork();
if (pid == 0) {
break;
}
}
printf("当前进程pid: %d\n", getpid());
return 0;
}
进程回收
wait
这是个阻塞函数,如果没有子进程退出,函数会一直阻塞等待。当检测到子进程退出,该函数解除阻塞,回收子进程资源。这个函数调用一次,只能回收一个子进程的资源,如果有多个子进程需要资源回收,函数需要被调用多次。
pid_t wait(int *status);
取出传出参数中的信息需要使用一些宏函数:
- WIFEXITED(status):返回1, 进程是正常退出的
- WEXITSTATUS(status):得到进程退出时候的状态码,相当于return后边的数值,或者exit函数的参数
- WIFSIGNALED(status):返回1, 进程是被信号杀死的
- WTERMSIG(status):获得进程是被哪个信号杀死的,会得到信号的编号
返回值:
- 成功:返回被回收的子进程的进程 ID
- 失败: -1,没有子进程资源可以回收了,函数的阻塞会自动解除;回收子进程资源的时候出现了异常
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid;
for (int i = 0; i < 5; ++i)
{
pid = fork();
if (pid == 0)
{
break;
}
}
if (pid > 0)
{
while (1)
{
pid_t ret = wait(NULL);
if (ret > 0)
{
printf("成功回收子进程资源,子进程PID: %d\n", ret);
}
else
{
printf("没有子进程资源可以回收了或出现了异常\n");
break;
}
printf("我是父进程, pid=%d\n", getpid());
}
}
else if (pid == 0)
{
printf("我是子进程,pid=%d,父进程ID: %d\n", getpid(), getppid());
}
return 0;
}
waitid
该函数可以控制回收子进程资源的方式是阻塞还是非阻塞,另外还可以精确指定回收某个或者某一类或者是全部子进程资源。这个函数调用一次,只能回收一个子进程的资源,如果有多个子进程需要资源回收,函数需要被调用多次。
pid_t waitpid(pid_t pid, int *status, int options);
参数:
- pid
- -1:回收所有的子进程资源,即无差别回收
- >0:指定回收子进程的进程ID
- 0:回收当前进程组的所有子进程
- <-1:其绝对值代表进程组ID,表示要回收这个进程组的所有子进程资源
- status
- options
- 0: 函数是阻塞的
- WNOHANG: 函数是非阻塞的
返回值:如果函数是非阻塞的,并且子进程还在运行,返回 0
- 成功:返回被回收的子进程的进程 ID
- 失败: -1,没有子进程资源可以回收了,函数的阻塞会自动解除;回收子进程资源的时候出现了异常
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid;
for (int i = 0; i < 5; ++i)
{
pid = fork();
if (pid == 0)
{
break;
}
}
if (pid > 0)
{
while (1)
{
int status;
pid_t ret = waitpid(-1, &status, WNOHANG);
if (ret > 0)
{
printf("成功回收了子进程资源,子进程PID: %d\n", ret);
if (WIFEXITED(status))
{
printf("子进程退出时候的状态码: %d\n", WEXITSTATUS(status));
}
if (WIFSIGNALED(status))
{
printf("子进程是被这个信号杀死的: %d\n", WTERMSIG(status));
}
}
else if (ret == 0)
{
}
else
{
printf("没有子进程资源可以回收了或者出现异常\n");
break;
}
}
}
else if (pid == 0)
{
printf("我是子进程,pid=%d,父进程ID: %d=====\n", getpid(), getppid());
}
return 0;
}
孤儿进程
当父进程先结束,而子进程还在运行,子进程此时变成孤儿进程。子进程退出时, 进程的用户区可以自己释放,但是内核区的PCB资源无法释放,必须要由父进程释放。当检测到某一个进程变成了孤儿进程,孤儿进程会自动被init进程收养,由init进程收集进程状态信息,回收进程资源。
僵尸进程
当子进程退出时,内核释放该进程所有的资源,但仍为其保留一定的信息(进程ID、退出状态、运行时间等),直到父进程通过wait或者waitpid函数来获取子进程状态信息后才释放。如果父进程不收集子进程状态信息, 那么保留的信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量地产生僵尸进程,系统将无可用的进程号。
如何消除僵尸进程
- 使用kill命令发送
SIGKILL 信号,无条件杀死父进程,僵尸进程就变成了孤儿进程,会被init进程接管。 - 子进程退出时向父进程发送
SIGCHILD 信号,父进程在信号处理函数中调用wait或者waitpid函数处理僵尸进程。
参考:https://subingwen.cn/linux/process/ 参考:https://blog.csdn.net/gogokongyin/article/details/51178257 参考:https://github.com/twomonkeyclub/BackEnd/tree/master/
|