IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 【Linux】多进程 -> 正文阅读

[系统运维]【Linux】多进程

虚拟地址空间

虚拟地址空间将程序和物理内存隔离开,程序中访问的内存地址不是实际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到适当的物理内存地址上。
在这里插入图片描述

  1. 内核区
  • 内核空间不允许应用程序读写
  • 内核驻留在内存中
  • 系统中所有进程对应的虚拟地址空间的内核区都会映射到同一块物理内存上
  1. 用户区
  • (1)保留区: 未赋予物理地址,任何对它的引用都是非法的,程序中的空指针指向的内存地址。
  • (2).text段: 代码段,存放程序的执行代码 (CPU执行的机器指令),代码段是只读的。
  • (3).data段: 数据段,存放程序中已初始化且初值不为0的全局变量和静态变量。
  • (4).bss段: 存放程序中未初始化以及初始为0的全局变量和静态变量,操作系统把这些未初始化的变量初始化为0。
  • (5)堆区:用于存放进程运行时动态分配的内存。由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。堆向高地址扩展,由于系统用链表来存储空闲内存地址,是不连续的内存区域,堆中内容只能通过指针间接访问。
  • (6)内存映射区:磁盘文件映射或程序运行过程中需要调用的动态库。
  • (7)栈区: 存储函数内部的非静态局部变量、函数参数等信息,栈内存由编译器自动分配释放。栈向低地址扩展,分配的内存是连续的。
  • (8)命令行参数:存储进程执行时传递给main函数的参数。
  • (9)环境变量: 存储和进程相关的环境变量,比如:工作路径,进程所有者等信息。

exec族系统调用

exec族系统调用用于执行一个可执行程序,执行成功后不会返回,因为执行该系统调用的进程,虚拟内存空间的用户区(包括代码段、数据段、堆、栈等)都被新内容替代。我们一般在子进程中调用 exec 族函数,子进程的用户区被替换掉开始执行新程序中的代码逻辑,而父进程不受任何影响仍然可以继续正常工作。

// path:新的可执行程序的路径,推荐使用绝对路径
// arg:使用命令ps aux查看进程信息时,启动的进程的名字。可以随意指定,一般和可执行程序名相同
// ...:要执行的命令需要的参数,可以写多个,最后以NULL结尾,表示参数指定完毕。
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); // execl("/bin/ps", "title", "a", "u", "x", NULL);
#else
        // 已经设置了环境变量的可执行程序
        execlp("ps", "title", "aux", NULL); // execl("ps", "title", "a", "u", "x", 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 // pid == -1
    {
        // 子进程创建失败
    }
    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

这是个阻塞函数,如果没有子进程退出,函数会一直阻塞等待。当检测到子进程退出,该函数解除阻塞,回收子进程资源。这个函数调用一次,只能回收一个子进程的资源,如果有多个子进程需要资源回收,函数需要被调用多次。

// status:传出参数,通过传递出的信息判断回收的进程是怎么退出的,如果不需要该信息可以指定为NULL。
pid_t wait(int *status);

取出传出参数中的信息需要使用一些宏函数:

  • WIFEXITED(status):返回1, 进程是正常退出的
  • WEXITSTATUS(status):得到进程退出时候的状态码,相当于return后边的数值,或者exit函数的参数
  • WIFSIGNALED(status):返回1, 进程是被信号杀死的
  • WTERMSIG(status):获得进程是被哪个信号杀死的,会得到信号的编号

返回值:

  • 成功:返回被回收的子进程的进程 ID
  • 失败: -1,没有子进程资源可以回收了,函数的阻塞会自动解除;回收子进程资源的时候出现了异常
// wait 函数回收子进程资源
#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);

参数:

  1. pid
  • -1:回收所有的子进程资源,即无差别回收
  • >0:指定回收子进程的进程ID
  • 0:回收当前进程组的所有子进程
  • <-1:其绝对值代表进程组ID,表示要回收这个进程组的所有子进程资源
  1. status
  • 与wait函数一致
  1. options
  • 0: 函数是阻塞的
  • WNOHANG: 函数是非阻塞的

返回值:如果函数是非阻塞的,并且子进程还在运行,返回 0

  • 成功:返回被回收的子进程的进程 ID
  • 失败: -1,没有子进程资源可以回收了,函数的阻塞会自动解除;回收子进程资源的时候出现了异常
// waitpid() 非阻塞回收多个子进程资源
#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/

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-05-05 12:01:58  更:2022-05-05 12:02:42 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 17:25:39-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码