• 进程的创建与替换


    进程创建

    #include 
    pid_t fork(void);
    返回值:子进程中返回0,父进程返回子进程pid,出错返回-1
    
    • 1
    • 2
    • 3

    因为一个父亲可能有多个孩子,所以为了区分孩子,父进程fork的返回值是孩子的进程pid;

    一个孩子只有唯一一个父亲,所以fork之后返回0的部分,就是child的部分;

    #include
    #include
    using namespace std;
    int global = 10;
    int main()
    {
        pid_t pid = fork();
        if(pid==0){
            //global = 1;
            cout<<"i am child"<<"global = "<<&global<<" : "<<global<<endl;
        }
        else if(pid >0){
             cout<<"i am child"<<"global = "<<&global<<" : "<<global<<endl;
        }
        else cerr<<"error"<<endl;
    
        
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    写时拷贝

    通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

    在这里插入图片描述

    在这里插入图片描述

    • 不修改值时:可以看到父子进程的global变量地址和值都一样;

    • 可是子进程修改了这个global变量了,结果如下:

    在这里插入图片描述

    我们发现他们的值确实是不一样了,但是地址还是一样的,如果这个地址是真实的物理内存,这显然是不现实的;

    所以这个地址是一个虚拟内存,操作系统为每个进程分配的都是虚拟内存,都在画大饼,每个进程都是0~ffffffff 4G的内存(32位下),真实的物理内存,是通过虚拟内存和页表结合映射才能找到;

    进程虚拟地址空间

    在这里插入图片描述

    OS使用虚拟内存的好处:

    • 防止用户直接使用物理内存,这是一件很危险的事;(修改了敏感内存的系统核心数据)
    • 让每个进程有独立的地址空间; (进程都使用虚拟内存,虚拟内存冲突了也没事,会通过MMU映射到唯一的不会冲突的实际物理内存)

    进程等待

    • 上述fork进程的代码,父进程没有等待子进程;
    • 这样做如果子进程退出的早,没人回收了,会产生僵尸进程,导致内存泄漏,并且任何方法都杀不死僵尸进程;(父进程先退出,会产生孤儿进程,会被os领养释放)
    //wait函数的功能是等待任意 一个 进程退出,并获取该进程的退出状态。 多线程需要循环调用;
    #include 
    #include 
    pid_t wait(int*status);
    //waitpid函数等待一个进程退出,并拿到status状态,可以设置阻塞or非阻塞options的方式;;WNOHANG非阻塞轮训等待方式的过程中,父进程可以做别的事;
    pid_t waitpid(pid_t pid, int *status状态, int options);
    //参数:pid>0,等一个确定的子pid,pid=-1 等任意一个子进程;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    status参数

    我们发现,不管是哪个等待函数,都会有一个int*status参数,这其实是一个输出型参数; 是为了父进程拿到子进程的退出码或者终止信号,便于分析子进程的完成状态;

    分为正常终止–>退出码exit(x) 和 收到信号异常终止–>SIGN=X:

    在这里插入图片描述

    我们可以用位操作的形式,拿到status高8位的正常退出码,也可以用下列了两个宏方便操作:

    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
    WEXITSTATUS(status): 若WIFEXITED非零(正常退出),提取子进程退出码。(查看进程的退出码)

    if(WIFEXITED(status))//退出信号为0,子进程正常退出
    int code = WEXITSTATUS(status);//子进程退出码
    
    • 1
    • 2

    core dump标志位,在子进程异常终止后,置为1,我们会发现产生一个新的文件“core”,里面记录了异常原因;

    当一个进程要异常终止时,可以选择把用户空间的内存数据全部保存到磁盘上,文件名通常是core,这个过程就叫做core dump。进程异常终止通常是因为有bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因;

    进程替换

    有了进程创建,引入进程替换:

    一般来说可以创建一个进程,然后替换掉子进程,这样可以保证父进程继续执行常规逻辑的前提下,子进程执行特定的替换程序;

    进程替换是不会创建新的进程的!exec系列的函数让特定进程读取加载磁盘中的其他程序;

    进程替换原理

    子进程在创建后会有一块自己的进程地址空间(虚拟内存),同时通过页表映射到物理内存上,而进程替换的原理就是通过改变页表的映射关系将子进程的进程地址空间映射到要替换的程序所对应的物理内存上即可,此时子进程要执行的程序就被替换了。

    在这里插入图片描述

    而在进程替换的过程中,本质上并没有创建新的进程,因为虽然子进程执行的程序被替换了(修改了页表映射关系),但子进程自己的pid并没有发生变化

    进程替换函数

    //exec系列进程替换函数
    #include 
    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 execvpe(const char *file, char *const argv[], char *const envp[]);
    int execve(const char *filename, char *const argv[], char *const envp[]);
    
    l(list) : 表示参数采用列表
    v(vector) : 参数用数组
    p(path) : 有p自动搜索环境变量PATH
    e(env) : 表示自己维护环境变量
    - 这些函数若执行成功则程序被替换,后续代码不再执行。
    - 若调用出错则返回-1
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    下列代码展示替换进程:

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main()
    {
      pid_t id = fork();
      if(id == 0)
      {
        //child
        printf("I am child. pid: %d,ppid:%d\n",getpid(), getppid());
        execl("test", NULL);//替换接口; NULL代表输入完了命令行参数!
        exit(1);//调用失败
      }
    
      int status = 0;
      int ret = waitpid(-1, &status, 0);
      if(WIFEXITED(status))//子进程正常退出
      {
        int code = WEXITSTATUS(status);//获取子进程退出码
        printf("wait successfully, exit code: %d, pid: %d\n", code, ret);
      }
      else
      {
        printf("wait failed,sig: %d\n", (status & 0x7F));
      }
      return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    在这里插入图片描述

    可以看到,对于execl函数,第一个参数是要替换的程序所在的路径其后为可变参数列表,即为在命令行上要执行的命令及选项,这种传参方式叫做以列表方式传参,即list方法;最后需以NULL结尾,告知exec系列函数传参结束。

    //execv函数接口就不是以列表的形式传入参数了,而是通过数组(vector)的形式传入参数,当然两者的最终结果并无区别。
    char* const my_argv[] = { "ls", "-l", "-a", NULL};
    execv("/usr/bin/ls", my_argv);
    
    //有p会自动搜索环境变量PATH(只有系统命令或我们导入到PATH路径下的命令可以搜索),这也就意味着,在第一个参数中对于要替换的指令我们并不需要带上路径,而后面的参数则与execl函数接口的参数一致;
    execlp("ls", "ls", "-l", "-a", NULL);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 通过上面的内容我们知道程序替换就是通过exec系列的函数让特定进程取加载磁盘中的其他程序,以达到运行的目的,并且期间不创建新的进程。

    • 如果子进程有新的程序需求就需要进程替换。

    • exec系列函数只要返回了,就说明函数调用失败了。

  • 相关阅读:
    vue超好用的自定义指令封装
    猿创征文|软件测试入门(一)
    centos7 升级内核
    在构建 Web3 前,需先知道什么是区块链,毕竟 Web3 是基于区块链
    社区矫正手机智能管控平台
    windows电脑上传ipa到appstore的步骤说明
    [模拟]绿地围栏 2022RoboCom决赛A
    编译后的go程序无法在alpine基础镜像创建的容器运行问题
    【LeetCode每日一题】——130.被围绕的区域
    Python进阶教程:pandas数据分析实践示例总结
  • 原文地址:https://blog.csdn.net/wtl666_6/article/details/127713048