• Linux:进程管理 | 进程创建 | 进程终止 | 进程等待 | 进程替换



    全文约 8351 字,预计阅读时长: 22分钟


    进程创建

    1. 命令行启动命令(程序、指令等)
    2. 通过程序自身fork出来的子进程
    • OS是进程、与内存的管理者。

    fork

    • . fork是系统提供的调用接口;进程调用fork,当控制转移到内核中的fork代码后,内核做:
      1. 分配新的内存块和内核数据结构给子进程;
      2. 将父进程部分数据结构内容拷贝至子进程;
      3. 添加子进程到系统进程列表当中;
      4. fork返回,开始调度器调度
        在这里插入图片描述

    创建子进程,本质是多了一个进程;多了一个进程是多了一套进程相关的数据结构。

    • 所有fork出来的子进程大部分数据,都是以父进程为模板拷贝的
    • 再不写入的情况下,用户的代码和数据是父子只读共享的
    • 为什么是共享的?
      • 因为子进程没有加载代码数据的过程,只能用父进程的。
      • PC计数器也是共享的,fork这条指令执行的时候,PC已经指向下一条指令了。所以只能执行后面的代码
    int main()
    {
    	const char* str ="hello world\n";
    	fork();//父进程调用fork
    	//子进程只会执行后面的代码
    	while(1)
    	{
    		printf(" pid: %s,ppid: %s, str:%s\n",getpid(),getppid(),str);
    		sleep(1);
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • fork 常规用法:
      • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
      • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
    • fork 调用失败的原因:
      • 系统中统中有太多的进程
      • 实际用户的进程数超过了限制

    写时拷贝机制

    • 一个变量里面,为什么会有两个不同的值?从而让父子进入不同的业务逻辑。
    int main()
    {
    	const char* str ="hello world\n";
    	pid_t ret = fork();//父进程调用fork
    	//子进程只会执行后面的代码
    	if(ret==0)
    	{
    		while(1)
    		{
    		printf("child: pid: %s,ppid: %s, str:%s\n",getpid(),getppid(),str);
    		sleep(1);
    		}
    	}
    	else if(ret>0)//返回子进程的PID
    	{
    		while(1)
    		{
    		printf("father: pid: %s,ppid: %s, str:%s\n",getpid(),getppid(),str);
    		sleep(1);
    		}
    	}
    	else
    	{
    		perror("fork");
    	}
    	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
    • 通常,父子代码共享,父子再不写入时,数据也是共享的、只读的;当任意一方试图写入,便以写时拷贝的方式各自一份副本。
      • 在fork返回之前,子进程已经创建出来了,也需要创建变量接受,将返回值写入变量里。
      • 父子页表的映射数据映射到了不同的内存区域
        在这里插入图片描述
    • 父子进程其中一个要修改数据时,页表层会有一个报错,OS拦截下来;看看是什么原因报错(野指针、越界等);
      • 一看是父子进程某一个写入,行吧,我知道了,给你拷贝一份和原数据一样大的空间…
    1. 所有的数据,子进程都拷贝一份不就好了吗?
      (1). 不行,父进程有很多只读的数据,而子进程只需要修改其中的一些或几个。如果全拷贝,浪费内存和系统资源。
      (2).fork 时,创建数据额结构,如果全拷贝,会降低fork的效率。
      (3).fork 本身就要像系统要更多的资源,要的太多可能会导致 fork 失败。

    进程终止

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

    退出码

    • 为何main函数中,总是返回0?
      • 给系统看,代表进程退出,0表示成功运行。
        在这里插入图片描述
    • 退出码:可以人为的定义,也可以使用系统的错误码list。
    #include
    #include
    int main()
    {
    	for(int i=0;i<135;++i)
    	{
    		printf("[%d]:%s\n",i,strerror());
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 当程序运行失败时,最关心程序为什么失败;退出码就承担着int—>string(错误信息描述)的转换。
    • 父进程可以关心子进程的运行,也可以不关心。
    • 在进程非正常结束的情况下,退出码毫无意义。

    进程退出方法

    • main 函数的return ;任何函数的 exit 。
      • 非main 函数的 return 不是终止进程,而是结束函数;
      • 任何函数的exit ,都表示终止进程。
    • exit 在退出的时候,会进行后续资源处理,包括刷新缓冲区;_exit 则不会。man 2/3 exit

      站在操作系统角度,理解终止进程,核心思想:

    1. “释放”曾经为了管理进程所维护的所有数据结构对象。…释放:不是真的把数据结构对象销毁,而是设置成不用的状态,然后保存起来;如果这样的不用的对象多了,就会有一个“数据结构的池。”
    2. 释放程序代码和数据占用的内存空间。…不是把代码数据清空,而是把改内存空间设置成无效就可以。
    3. 取消曾经该进程的连接关系。…双向链表的指向关系取消。
      在这里插入图片描述

    - C语言也是有内存清理资源的函数。


    进程等待

    1. 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
    2. 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
    3. 如果不存在该子进程,则立即出错返回。
    • pid_t wait ( int* status );父进程调用等待。
      • 等待任意一个进程;
      • 返回值是子进程的PID;失败返回-1
      • status:操作系统在PCB里面存着的退出码(比特位第15位到第8位),需要 (status>>8) & 0xFF获得
      • status:如果传递NULL,表示不关心子进程的退出状态信息。
      • 多个子进程的情况,利用循环调用等待子进程。
    #include
    #include 
    #include 
    #include 
    #include 
    
    int main()
    {
        pid_t id = fork();
        if(id == 0)    
        {    
            int count=5;    
            while(count)    
            {    
                printf("child: i am running %d, ppid: %d, pid: %d\n",count--,getppid(),getpid());    
                sleep(1);    
            }    
            printf("child: i quit....\n");    
            exit(10);    
        }    
        else    
        {                                                                                                                                             
            printf("father is  waiting:\n");    
            int status =0;    
            pid_t ret = wait(&status);    
            printf("father waited done :exit num: %d, ret: %d\n",(status>>8) & 0xFF,ret);//拿到 status 的第15 - 8的比特位    
            printf("father quit ...\n");
        }
        //if(ret == -1)...
        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
    • pid_ t waitpid ( pid_t pid , int * status, int options);
      • options:OS在PCB存着的退出信号:kill -num
        • 给 0 意味着采用阻塞等待的方式调用 waitpid;退出信号存放在status的比特位第6位到第0位,通过status & 0X7F获得
        • WNOHANG,表示采用非阻塞等待轮询探测调用 waitpid;
        • WNOHANG:pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待;父进程继续去做别的事情。
        • 若正常结束,则waitpid()返回该子进程的ID。
      • 参数:pidPid=-1,等待任一个子进程,与wait等效。
        • Pid>0.等待其进程ID与pid相等的子进程。
    • 返回值:当正常返回的时候waitpid返回收集到的子进程的进程ID;
      • 如果设置了选项WNOHANG,而调用中waitpid发现没有子进程可收集,则返回0;
      • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
    • 阻塞等待:
    //childe
    	pid_t id = fork();
        if(id == 0)    
        {    
            int count=5;    
            while(count)    
            {    
                printf("child: i am running %d, ppid: %d, pid: %d\n",count--,getppid(),getpid());    
                sleep(1);    
            }    
            printf("child: i quit....\n");    
            exit(10);    
        } 
    
    //father
     int status =0;
     pid_t ret = waitpid(-1,&status,0);  
    	 if(ret >0 && WIFEXITED(status)== 0)
        {
            printf("wait sucess quit normal !\n");
            printf("exit code: %d,quit signal: %d, pid: %d\n",WEXITSTATUS(status),WIFEXITED(status),ret);
        }
        else{
            printf("wait failed  child quit kill signal:%d\n", WIFEXITED(status));//推出信号
        }
    
    • 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
    • 为什么要让进程等待:
      • 回收僵尸进程,解决内存泄漏;
      • (不是必须的)需要获取进程的运行结束状态
      • 尽量让父进程晚于子进程退出,可以规范化进行回收资源
    • status的系统提供的使用方法
      • WIFEXITED(status): 若为正常终止子进程,终止信号返回 0。
      • WEXITSTATUS(status): 若子进程非正常终止,提取子进程退出码。

    阻塞、非阻塞的等待

    • 阻塞等待:父进程调用 waitpid(id,&status,0)时,会一直等着子进程做完事情,什么事情也做不了。
      • 父进程从R状态进入非R状态,进入等待队列;
      • 直到子进程执行完毕,OS唤醒父进程,再将父进程加入调度队列。由OS完成一系列操作。
    • 非阻塞等待或非阻塞轮询检测方案:
      • 探测失败:子进程还在运行,下次在检测,父进程继续做事情…
      • 探测成功:获得ID
      • 真失败…,返回-1
    • WNOHANG的方式:
    //child
      pid_t id = fork();
        if(id == 0)    
        {    
            int count=5;    
            while(count)    
            {    
                printf("child: i am running %d, ppid: %d, pid: %d\n",count--,getppid(),getpid());    
                sleep(1);    
            }    
            printf("child: i quit....\n");    
            exit(10);    
        } 
    //father
    	int status =0;
        pid_t ret = waitpid(-1,&status,WNOHANG);  
        while(1)
       {
        	if(ret == 0)
       	  {
            printf("wait next !\n"); 
          	printf("fatehr do other thing\n");
     	  }
        else if(ret >0)
          {
            printf("wait sucess \n");
              printf("exit code: %d,quit signal: %d, pid: %d\n",WEXITSTATUS(status),WIFEXITED(status),ret);
              break;
          }
        else
        {
        	 printf("wait failed\n");
        	 break;
        }
      }
    
    • 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
    • 32
    • 33
    • 34
    • 35

    进程替换

    • 创建子进程的目的:
      • 执行父进程的代码
      • 执行其他程序的代码
    • 进程程序替换也就是把原来父进程的代码替换成其他程序的代码,执行全新的程序。
    • 程序 = 代码 + 数据,程序在磁盘就是一个普通的可执行文件。
    • 如何替换?OS完成:
      • 通过中断,信号等触发写实拷贝机制,OS在物理内存上重新开辟一块儿代码空间和一块儿数据空间;页表重新构建子进程的进程地址空间与物理内存之间的关系,再把新程序的代码数据加载到内存…

    替换函数

    • exec函数算是一种特殊的加载器。
      在这里插入图片描述
    • 有六种以exec开头的函数,统称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 execve(const char *path, char *const argv[], char *const envp[]);
    int execvpe(const char *file, char *const argv[],char *const envp[]);
    
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
      • 如果调用出错则返回-1
      • 所以exec函数只有出错的返回值而没有成功的返回值。
    • 命名理解:
      • l(list) : 表示参数采用列表
      • v(vector) : 参数用数组
      • p(path) : 有p自动搜索环境变量PATH
      • e(env) : 表示自己维护环境变量

    • 事实上,只有execve真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。

    exec l

    • 一:int execl(const char *path, const char *arg, ...);
      在这里插入图片描述
    int main()
    {
    	printf("my process begined.....\n");
    	execl("/user/bin/ls","ls","-a",NULL)//要以空 结尾
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    exec lp

    • 二:int execlp(const char *file, const char *arg, ...);
    #include 
    #include
    #include 
    #include
    #include
    
    int main()
    {
        pid_t rid=fork();
        if(rid==0)
        {
            int cou=5;
            while(cou--)
            {
                printf("child: %d, ppid: %d,read to run other,%d\n",getpid(),getppid(),cou);
                sleep(1);
            }
            execlp("top","top",NULL);
            exit(11);
        }
        int status =0;
        pid_t ret = waitpid(-1,&status,0);
        if(ret >0 && WIFEXITED(status)== 0)//信号
        {
            printf("wait sucess !\n");
            printf("exit code: %d,quit signal: %d, pid: %d\n",WEXITSTATUS(status),WIFEXITED(status),ret);
        }
        else{
            printf("wait failed quit signal: %d\n", WIFEXITED(status));
        }
        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
    • 32

    exec le

    • 三:int execle(const char *path, const char *arg, ...,char *const envp[]);
    #include 
    #include
    #include 
    #include
    #include
    int main(int argc,char* argv[],char* env[])
    {
        pid_t rid=fork();
        if(rid==0)
        {
            int cou=5;
            while(cou--)
            {
                printf("child: %d, ppid: %d,read to run other,%d\n",getpid(),getppid(),cou);
                sleep(1);
            }
            execle("/usr/bin/ls","ls","-i",NULL,env);
            exit(11);
        }
    		.....
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    exec ve

    • 四:int execve(const char *path, char *const argv[], char *const envp[]);
      • 调用自己写的 t1 程序,t1 打印获取传过来的自定义环境变量。
    //T1.C
    #include 
    #include 
    int main()
    {
        int i=0;
        for(;i<2;++i)
        {
            printf("i am here hh, test for execve(); use getenv(...)\n");
        }
    		printf("environment variable: %s\n",getenv("MY_ENV"));
        return 0;
    }
    ---------------------
    int main()
    {
        pid_t rid=fork();
        if(rid==0)
        {
            int cou=5;
            while(cou--)
            {
                printf("child: %d, ppid: %d,read to run other,%d\n",getpid(),getppid(),cou);
                sleep(1);
            }
            char* const my_argv[]={
             "t1",
             NULL
            };
            char* const my_env[]={
                "MY_ENV= hello execve!",
                NULL
            };
    		execve("./t1",my_argv,my_env);
            exit(11);
       }
       ....
    }
    
    • 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
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    exec vpe

    • 五:int execvpe(const char *file, char *const argv[], char *const envp[]);
    • 调用自己写的 t1 程序,t1 打印获取传过来的环境变量。
    [saul@VM-12-7-centos tt729]$ export my_env="hello execvpe hh cannot add gantanhao"  ...自定义的环境变量
    [saul@VM-12-7-centos tt729]$ PATH=$PATH:/home/saul/tt729                        ..运行程序不用加 。/
    [saul@VM-12-7-centos tt729]$ vim t1.c
    //t1。c
    #include 
    #include 
    int main()
    {
        int i=0;
        for(;i<2;++i)
        {
            printf("i am here hh, test for execvpe(); use getenv(...)\n");
        }
        printf("environment variable: %s\n",getenv("my_env"));
        return 0;
    }
    //main
    int main(int argc,char* argv[],char* env[])
    {
        pid_t rid=fork();
        if(rid==0)
        {
            int cou=5;
            while(cou--)
            {
                printf("child: %d, ppid: %d,read to run other,%d\n",getpid(),getppid(),cou);
                sleep(1);
            }
            char* const my_argv[]={
             "t1",
             NULL
            };
            execvpe("t1",my_argv,env);
            exit(11);
        }
    
    }
    
    • 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
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    exec vp

    • 六:int execvp(const char *file, char *const argv[]);
    int main()
    {
     char *const argv[] = {"ps", "-ef", NULL};
    // 带p的,可以使用环境变量PATH,无需写全路径
     execvp("ps", argv);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • v:exec参数列表:传的是:该程序命令行参数列表的字符指针数组,
    • p:自动获取全局变量PATH;exec参数列表:前面的第一个参数可以不加路径;
    • l:在exec 参数列表一个个传过去 程序的使用方法。l , v 都以 NULL 结尾。
    • e:在exec 参数列表:
      • 传过去继承的环境变量字符指针数组、
      • 或自己定义的env 环境变量的字符指针数组
    • 可以看出,通过这种exec*函数的方式可以在一个语言程序内,跑另外一个语言的程序。

    exec v

    • 七:int execv(const char *path, char *const argv[]);
    int main()
    {
     char *const argv[] = {"ps", "-ef", NULL};
    execv("/user/bin/ps", argv);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    寄语

    • 当用户层面某个程序很卡(非网络问题)时,不是进程过多,CPU忙不过来;就是当前进程进入了等待队列,此时会提示你继续等待还是退出的框框弹出来…
      - …有点饶
  • 相关阅读:
    Converting HTML to PDF in .NET 7 Crack
    QT : 完成绘制时钟
    Numpy数组中d[True]=1的含义
    Synchronized锁1
    【2023秋招面经】4399 前端 二面-hr面(20min)
    GDB调试程序常用命令
    DGIOT国内首家轻量级物联网开源平台——MQTT接入实战教程
    竣达技术 | 适用于”日月元”品牌UPS微信云监控卡
    thinkphp执行复杂存储过程,有时候成功,有时候失败的解决办法
    微信最新更新隐私策略(2023-08-15)
  • 原文地址:https://blog.csdn.net/WTFamer/article/details/126029713