进程控制相关


一、进程相关概念

1.1 程序和进程

  • 程序,是指编译好的二进制文件,在磁盘上,占用磁盘空间, 是一个静态的概念。

  • 进程,一个启动的程序, 进程占用的是系统资源,如:物理内存,CPU,终端等,是一个动态的概念。

1.2 并行和并发

  • 并发:在一个时间段内, 是在同一个cpu上, 同时运行多个程序。
    如:若将CPU的1S的时间分成1000个时间片,每个进程执行完一个时间片必须无条件让出CPU的使用权,这样1S中就可以执行1000个进程。

  • 并行性指两个或两个以上的程序在同一时刻发生。

1.3 PCB-进程控制块

  每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。

  task_struct结构体定义。其内部成员有很多,重点掌握以下部分即可:

  • 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。

  • 进程的状态,有就绪、运行、挂起、停止等状态。

  • 进程切换时需要保存和恢复的一些CPU寄存器。

  • 描述虚拟地址空间的信息。

  • 描述控制终端的信息。
    当前工作目录(Current Working Directory)
    getcwd --pwd

  • umask掩码。

  • 文件描述符表,包含很多指向file结构体的指针。

  • 和信号相关的信息。

  • 用户id和组id

  • 会话(Session)和进程组。

  • 进程可以使用的资源上限(Resource Limit)
    ulimit -a

1.4 进程状态(重点)

  进程基本的状态有5种。分别为初始态,就绪态,运行态,阻塞态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看。

Note:

  • 只有就绪态运行态可以相互转换,其它的都是单向转换。就绪态的进程通过调度算法从而获得
    CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪态,等待下一次调度。
  • 阻塞态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态

二、创建进程

2.1 fork函数

通过man 2 fork查看fork函数:
在这里插入图片描述

  • 函数作用:创建子进程

  • 原型: pid_t fork(void);
    函数参数:无

  • 返回值:
    调用成功:父进程返回子进程的PID,子进程返回0
    调用失败:返回-1,设置errno值。

  • fork函数代码片段实例
    在这里插入图片描述

  • 调用fork函数的内核实现原理:
    在这里插入图片描述

fork函数总结:

  • fork函数的返回值?
    ① 父进程返回子进程的PID,是一个大于0数;
    ② 子进程返回0;
    特别需要注意的是:不是fork函数在一个进程中返回2个值,而是在父子进程各自返回一个值。

  • 子进程创建成功后,代码的执行位置?
    父进程执行到什么位置,子进程就从哪里执行

  • 如何区分父子进程?
    通过fork函数的返回值

  • 父子进程的执行顺序
    无固定谁先谁后,哪个进程先抢到CPU,哪个进程就先执行。

fork函数测试:

//fork函数
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>

int main()
{
    
    
        printf("before fork,pid:[%d]\n",getpid());
        //创建子进程
        //pid_t fork(void)
        pid_t pid = fork();
        if(pid<0)//fork失败的情况
        {
    
    
                perror("fork error");
                return -1;
        }
        else if(pid>0)//父进程
        {
    
    
                printf("father: pid==[%d],fpid==[%d]\n",getpid(),getppid());
        //      sleep(1);
        }
        else if(pid==0)//子进程
        {
    
    
                printf("child: pid==[%d],fpid==[%d]\n",getpid(),getppid());
        }

        printf("after fork,pid:[%d]\n",getpid());
        return 0;
}

在这里插入图片描述

2.2 ps命令和kill命令

ps aux | grep "xxx"
ps ajx | grep "xxx"
-a:(all)当前系统所有用户的进程
-u:查看进程所有者及其他一些信息
-x:显示没有控制终端的进程 – 不能与用户进行交互的进程【输入、输出】
-j:列出与作业控制相关的信息
kill -l :查看系统有哪些信号
kill -9 pid :杀死某个线程

2.3 getpid / getppid

getpid - 得到当前进程的PID
pid_t getpid(void);

getppid - 得到当前进程的父进程的PID
pid_t getppid(void);

Note:所谓PID就是进程标识符

练习题

  • 编写程序,循环创建多个子进程,要求如下:
    1.多个子进程是兄弟关系。
    2.判断子进程是第几个子进程
//循环创建n个子进程---fork函数
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>

int main()
{
    
    
        int i = 0;
        for(i=0;i<3;i++)
        {
    
    
                //创建子进程
                //pid_t fork(void)
                pid_t pid = fork();
                if(pid<0)//fork失败的情况
                {
    
    
                        perror("fork error");
                        return -1;
                }
                else if(pid>0)//父进程
                {
    
    
                        printf("father: pid==[%d],fpid==[%d]\n",getpid(),getppid());
                //      sleep(1);
                }
                else if(pid==0)//子进程
                {
    
    
                        printf("child: pid==[%d],fpid==[%d]\n",getpid(),getppid());
                }       
        }       
        sleep(10);
        
        return 0;
}       

  下图为:循环创建 n个子进程的过程:

请添加图片描述

在这里插入图片描述

注意:若让多个子进程都是兄弟进程,必须不能让子进程再去创建新的子进程。

不让子进程继续创建子进程是这样的:

//循环创建n个子进程---fork函数
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>

int main()
{
    
    
        int i = 0;
        for(i=0;i<3;i++)
        {
    
    
                //创建子进程
                //pid_t fork(void)
                pid_t pid = fork();
                if(pid<0)//fork失败的情况
                {
    
    
                        perror("fork error");
                        return -1;
                }
                //else if(pid>0)//父进程
                //{
    
    
                //      printf("father: pid==[%d],fpid==[%d]\n",getpid(),getppid());
                //      sleep(1);
                //}
                else if(pid==0)//子进程
                {
    
    
                        //printf("child: pid==[%d],fpid==[%d]\n",getpid(),getppid());
                        break;
                }
                if(i==0)
                {
    
    
                        printf("first child,pid==[%d]\n",getpid());
                }
                if(i==1)
                {
    
    
                        printf("first child,pid==[%d]\n",getpid());
                }
                if(i==2)
                {
    
    
                        printf("first child,pid==[%d]\n",getpid());
                }
                if(i==3)
                {
    
    
                        printf("first child,pid==[%d]\n",getpid());
                }
        }

        return 0;
}

在这里插入图片描述

  • 编写程序,测试父子进程是否能够共享全局变量
    如下述程序:我们设置一个全局变量g_var,我们在父进程里使其+1,用来判断:
    我们打印子进程的g_var,如果g_var为100则说明父子进程是共享全局变量的,否则就不是。
    另外,如果子进程能够打印出g_var变量且为全局设定的99,则进一步说明:
    父子进程对全局变量:读时共享,写时复制
//父进程和子进程共享问题
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>

int g_var = 99;
int main()
{
    
    
        //创建子进程
        //pid_t fork(void)
        pid_t pid = fork();
        if(pid<0)//fork失败的情况
        {
    
    
                perror("fork error");
                return -1;
        }
        else if(pid>0)//父进程
        {
    
    
                printf("father: pid==[%d],fpid==[%d]\n",getpid(),getppid());
                printf("father中g_var地址: [%p]",&g_var);
                g_var++;
        //      sleep(1);
        }
        else if(pid==0)//子进程
        {
    
    
                sleep(1);//为了避免父进程还没执行,子进程已经结束了。
                printf("child: pid==[%d],fpid==[%d]\n",getpid(),getppid());
                printf("child: g_var==[%d]\n",g_var);
                printf("child中g_var地址: [%p]",&g_var);
        }

        printf("after fork,pid:[%d]\n",getpid());
        return 0;
}

在这里插入图片描述

三、exec函数族

3.1 函数作用和函数介绍

  有的时候需要在一个进程里面执行其他的命令或者是用户自定义的应用程序,此时就用到了exec函数族当中的函数。

  使用方法一般都是在父进程里面调用fork创建处子进程,然后在子进程里面调用exec函数。

3.1.1 execl函数

  • 函数原型: int execl(const char *path, const char *arg, ... /* (char *) NULL */);

  • 参数介绍:
    path: 要执行的程序的绝对路径
    变参arg: 要执行的程序的需要的参数
    arg:占位,通常写应用程序的名字
    arg后面的: 命令的参数
    参数写完之后: NULL

  • 返回值:若是成功,则不返回,不会再执行exec函数后面的代码;若是失败,会执行execl后面的代码,可以用perror打印错误原因。
    execl函数一般执行自己写的程序

如下面程序

#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>

int main()
{
    
    
        //创建子进程
        //pid_t fork(void)
        pid_t pid = fork();
        if(pid<0)//fork失败的情况
        {
    
    
                perror("fork error");
                return -1;
        }
        else if(pid>0)//父进程
        {
    
    
                printf("father: pid==[%d],fpid==[%d]\n",getpid(),getppid());
        //      sleep(1);
        }
        else if(pid==0)//子进程
        {
    
    
                printf("child: pid==[%d],fpid==[%d]\n",getpid(),getppid());
                //execl("/bin/ls","ls","-l",NULL);//ls所在位置用where ls查
                execl("./test1","l","i","n","u","x",NULL);
                perror("execl error");
        }

        return 0;
} 

test1.c程序

#include<stdio.h>
int main(int argc,char *argv[])
{
    
    
        int i = 0;
        for(i=0;i<argc;i++)
        {
    
    
                printf("[%d]:[%s]\n",i,argv[i]);
        }
        return 0;
}

在这里插入图片描述

3.1.2 execlp函数

  • 函数原型: int execlp(const char *file, const char *arg, .../* (char *) NULL */);

  • 参数介绍:
    file: 执行命令的名字, 根据PATH环境变量来搜索该命令
    arg:占位
    arg后面的: 命令的参数
    参数写完之后: NULL

  • 返回值:若是成功,则不返回,不会再执行exec函数后面的代码;若是失败,会执行exec后面的代码,可以用perror打印错误原因。
    execlp函数一般是执行系统自带的程序或者是命令

3.2 exec函数族原理介绍

exec族函数的实现原理图:
如:execlp(“ls”, “ls”, “-l”, NULL);
在这里插入图片描述

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>

int main()
{
    
    
        //创建子进程
        //pid_t fork(void)
        pid_t pid = fork();
        if(pid<0)//fork失败的情况
        {
    
    
                perror("fork error");
                return -1;
        }
        else if(pid>0)//父进程
        {
    
    
                printf("father: pid==[%d],fpid==[%d]\n",getpid(),getppid());
        //      sleep(1);
        }
        else if(pid==0)//子进程
        {
    
    
                printf("child: pid==[%d],fpid==[%d]\n",getpid(),getppid());
                //execl("/bin/ls","ls","-l",NULL);//ls所在位置用where ls查
                execlp("ls","ls","-l",NULL);//execlp不需要指定具体的位置
                //execlp("./test1","l","i","n","u","x",NULL);//execlp不需要指定具体的位置
                perror("execl error");
        }

        return 0;
}

在这里插入图片描述

总结:
exec函数是用一个新程序替换了当前进程的代码段、数据段、堆和栈;原有的进程空间没有发生变化,并没有创建新的进程,进程PID没有发生变化。

  • Note:当execlexeclp函数执行成功后,不返回,并且不会执行execl后面的代码逻辑,原因是调用execl函数成功以后,exec函数指定的代码段已经将原有的代码段替换了。

四、进程回收

4.1 为什么要进行进程资源的回收?

  当一个进程退出之后,进程能够回收自己的用户区的资源,但是不能回收内核空间的PCB资源,必须由它的父进程调用wait或者waitpid函数完成对子进程的回收,避免造成系统资源的浪费。

4.2 孤儿进程

  • 孤儿进程的概念:
      若子进程的父进程已经死掉,而子进程还存活着,这个进程就成了孤儿进程。

  为了保证每个进程都有一个父进程,孤儿进程会被init进程领养,init进程成为了孤儿进程的养父进程,当孤儿进程退出之后,由init进程完成对孤儿进程的回收。

  • 模拟孤儿进程的案例
    编写模拟孤儿进程的代码讲解孤儿进程,验证孤儿进程的父进程是否由原来的父进程变成了init进程。
//孤儿进程,保证父进程先死,子进程后死
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>

int main()
{
    
    
        //创建子进程
        //pid_t fork(void)
        pid_t pid = fork();
        if(pid<0)//fork失败的情况
        {
    
    
                perror("fork error");
                return -1;
        }
        else if(pid>0)//父进程
        {
    
    
                printf("father: pid==[%d],fpid==[%d]\n",getpid(),getppid());
        //      sleep(1);
        }
        else if(pid==0)//子进程
        {
    
    
                printf("child: pid==[%d],fpid==[%d]\n",getpid(),getppid());
                sleep(10);
                printf("child: pid==[%d],fpid==[%d]\n",getpid(),getppid());
        }
        return 0;
}

在这里插入图片描述

4.3 僵尸进程

  • 僵尸进程的概念:
    若子进程死了,父进程还活着, 但是父进程没有调用wait或waitpid函数完成对子进程的回收,则该子进程就成了僵尸进程。

  • 如何解决僵尸进程
    由于僵尸进程是一个已经死亡的进程,所以不能使用kill命令将其杀死
    通过杀死其父进程的方法可以消除僵尸进程。
    杀死其父进程后,这个僵尸进程会被init进程领养,由init进程完成对僵尸进程的回收。

僵尸进程测试:

//僵尸进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    
    
        //创建子进程
        pid_t pid = fork();
        if(pid<0) //fork失败的情况
        {
    
    
                perror("fork error");
                return -1;
        }
        else if(pid>0)//父进程
        {
    
    
                sleep(10);
                printf("father: [%d], pid==[%d], fpid==[%d]\n", pid, getpid(),getppid());
        }
        else if(pid==0) //子进程
        {
    
    

                printf("child: pid==[%d], fpid==[%d]\n", getpid(), getppid());
        }


        return 0;
}

测试如下图:zomble后为<defunct>代表此进程为僵尸进程
在这里插入图片描述

4.4 进程回收函数

4.4.1 wait 函数**

  • 函数原型:
    pid_t wait(int *status);

  • 函数作用:
    ① 阻塞并等待子进程退出
    ② 回收子进程残留资源
    ③ 获取子进程结束状态(退出原因)。

  • 返回值:
    成功:清理掉的子进程ID;
    失败:-1 (没有子进程)

  • status参数:子进程的退出状态 – 传出参数
    WIFEXITED(status):为非0 → 进程正常结束
    WEXITSTATUS(status):获取进程退出状态
    WIFSIGNALED(status):为非0 → 进程异常终止
    WTERMSIG(status):取得进程终止的信号编号。

wait函数练习
使用wait函数完成父进程对子进程的回收

//父函数调用wait函数完成对子进程的回收
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>

int main()
{
    
    
        //创建子进程
        //pid_t fork(void)
        pid_t pid = fork();
        if(pid<0)//fork失败的情况
        {
    
    
                perror("fork error");
                return -1;
        }
        else if(pid>0)//父进程
        {
    
    
                printf("father: pid==[%d],fpid==[%d]\n",getpid(),getppid());
                pid_t wpid = wait(NULL);
                printf("清理掉的子进程id,wpid==[%d]\n",wpid);
        }
        else if(pid==0)//子进程
        {
    
    
                printf("child: pid==[%d],fpid==[%d]\n",getpid(),getppid());
                sleep(5);
        }
        return 0;
}

在这里插入图片描述

4.1.2 waitpid 函数

  • 函数原型:
    pid_t waitpid(pid_t pid, int *status, in options);

  • 函数作用
    同wait函数

  • 函数参数
    参数:
    pid:
    pid = -1 等待任一子进程。与wait等效。
    pid > 0 等待其进程IDpid相等的子进程。
    pid = 0 等待进程组ID与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。
    pid < -1等待其组ID等于pid的绝对值的任一子进程。(适用于子进程在其他组的情况)
    status: 子进程的退出状态,用法同wait函数。
    options:设置为WNOHANG,函数非阻塞,设置为0,函数阻塞。

  • 函数返回值
    >0:返回回收掉的子进程ID;
    -1:无子进程
    =0:参3为WNOHANG,且子进程正在运行。

  • waitpid函数练习
    使用waitpid函数完成对子进程的回收

//父函数调用waitpid函数完成对子进程的回收
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>

int main()
{
    
    
        //创建子进程
        //pid_t fork(void)
        pid_t pid = fork();
        if(pid<0)//fork失败的情况
        {
    
    
                perror("fork error");
                return -1;
        }
        else if(pid>0)//父进程
        {
    
    
                printf("father: pid==[%d],fpid==[%d]\n",getpid(),getppid());
                int status;
                //pid_t wpid = wait(NULL);
                while(1)
                {
    
    
                        pid_t wpid = waitpid(-1,&status,0);//-1表示等待任意子进程,0代表阻塞,WNOHANG代>表不阻塞
                        //printf("wpid==[%d]\n",wpid);
                        if(wpid>0)//有子进程退出
                        {
    
    
                                if(WIFEXITED(status))//正常退出
                                {
    
    
                                        printf("child normal exit,status==[%d]\n",WEXITSTATUS(status));
                                }
                                else if(WIFSIGNALED(status))//被信号杀死
                                {
    
    
                                        printf("child killed by signal,signo==[%d]\n",WTERMSIG(status));
                                }
                        }
                        else if(wpid==0)//返回值:0 子进程还活着
                        {
    
    
                                //printf("child is living,wpid==[%d]\n",wpid);
                        }
                        else if(wpid==-1)//没有子进程了
                        {
    
    
                                printf("no child is living,wpid==[%d]",wpid);
                                break;
                        }
                }

        }
        else if(pid==0)//子进程
        {
    
    
                printf("child: pid==[%d],fpid==[%d]\n",getpid(),getppid());
                sleep(2);
                return 9;
        }

        return 0;
}

在这里插入图片描述

总结

期待大家和我交流,留言或者私信,一起学习,一起进步!

猜你喜欢

转载自blog.csdn.net/CltCj/article/details/123644579