linux下的进程(process)相关概念(定义、组成、状态、PCB、分类(僵尸、孤儿、守护进程)、进程的监控(查看命令))及软件开发相关面试问题

进程(process)

1、进程定义

  • 进程是 Unix 和 Linux 系统中对正在运行中的应用程序的抽象,通过它可以管理和监视程序对内存、处理器时间和 I / O资源的使用。程序被触发后,执行者的权限与属性、程序的程序代码与所需数据等都会被加载内存中,操 作系统并给予这个内存内的单元一个标识符 (PID),可以说,进程就是一个正在运作中的程序。
    面试热点
    -1、 进程,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位 或者linux操作系统最小的资源管理单元
  • 2、进程与程序对比
    在这里插入图片描述

2、进程的分类

按照进程的功能和运行的程序分类,进程可划分为两大类:

  • 1、系统进程
    可以执行内存资源分配和进程切换等管理工作,而且该进程的运行不受用户的干预,即使是root用户也不能干预系统进程的运行。
  • 2、用户进程
    通过执行用户程序、应用程序或内核之外的系统程序而产生的进程,此类进程可以在用户的控制下运行或关闭。针对用户进程,又可以分为如下3类:
    • 1、交互进程:由一个Shell终端其他的进程,在执行过程中,需要与用户进行交互操作,可以运行于前台,也可以运行于后台。
    • 2、批处理进程:该进程是一个进程集合,负责按顺序启动其他的进程。
    • 3、守护进程:守护进程是一直运行的一种进程,经常在Linux系统时启动,在系统关闭时终止。它们独立于控制终端且周期性地质学某种任务或等待处理某些发生的时间。例,httpd进程,crond进程等。

3、进程状态

在这里插入图片描述
一般操作系统将进程分为五个状态:

  • 1、新建:新建表示进程正在被创建。
  • 2、运行:运行是进程正在运行。
  • 3、阻塞:阻塞是进程正在等待某一个事件发生。
  • 4、就绪:就绪是表示系统正在等待CPU来执行命令。
  • 5、结束:完成表示进程已经结束了系统正在回收资源。

Linux上进程有5种状态,这5中状态可以与一般操作系统的状态对应起来:

  • 1、运行:正在运行或在运行队列中等待。
  • 2、中断:休眠中, 受阻, 在等待某个条件的形成或接受到信号。
  • 3、不可中断:收到信号不唤醒和不可运行, 进程必须等待直到有中断发生。
  • 4、僵死:进程已终止, 但进程描述符存在,直到父进程调用wait4()系统调用后释放。
  • 5、停止:进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行运行。

4、进程的组成

  • 1.正文段(text):存放程序代码。正文段具有只读的属性。
  • 2.用户数据段(user segment):是进程在运行过程中处理数据的集合,它们是 进程直接进行操作的所有数据(包括全部变量在内),以及进程使用的进程堆栈
  • 3.系统数据段(system segment):存放着进程的控制信息,即进程控制块(PCB ,Processing Control Block),名字为task_struct的数据结构。

说明:
PCB(进程控制块) 结构体 task struct,负责管理进程的所有资源,它的成员 mm_struct 指向这个进程相关的内存资源,mm_struct指向一个结构体,包括:

栈 :给局部变量(自动变量)分配空间的地方
堆 :使用malloc、new… 分配的空间(也叫自由区)
BSS段
数据段
代码段:代码区是只读的,程序代码会被读入此区,程序执行期间执行的就是代码区中的代码。

进程空间管理图片
在这里插入图片描述

4.1、正文段(text)和用户数据段

在这里插入图片描述

  • 1、用户数据段

Linux系统把进程的数据段又划分成三部分:

  • 1、用户栈区(供用户程序使用的信息区);
  • 2、用户数据区(包括用户工作数据和非可重入的程序段);
  • 3、系统数据区(包括系统变量和对换信息)

————————————————————————————————

  • 2、正文段
    程序段是可重入的程序,能被若干进程共享。为了管理可共享的正文段,Linux设置了一张正文表,每个正文段都占用一个表目,用来指出该正文段在内存和磁盘上的位置、段的大小以及调用该段的进程数等情况。

4.2、PCB进程控制块

在Linux成功fork进程后,会在系统中创建一个task_struct(也称PCB, process control block),用来描述当前进程的状态、进程间关系、优先级和资源等信息。
在这里插入图片描述

标识符: 与进程相关的唯一标识符,用来区别正在执行的进程和其他进程。
状态: 描述进程的状态,因为进程有挂起,阻塞,运行等好几个状态,所以都有个标识符来记录进程的执行状态。
优先级: 如果有好几个进程正在执行,就涉及到进程被执行的先后顺序的问题,这和进程优先级这个标识符有关。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 程序代码和进程相关数据的指针。
上下文数据: 进程执行时处理器的寄存器中的数据。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表等。
记账信息: 包括处理器的时间总和,记账号等等。

PCB中包含4个部分

  • 1.进程标识信息:用于唯一地标识一个进程,一个进程通常有两种标识符:
    • 内部标志符: 由操作系统赋予每个进程的一个唯一的数字标识符,它通常为一个进程的序号,方便了系统使用。
    • 外部标识符: 由创建者产生,是由字母和数字组成的字符串,为用户进程访问该进程提供方便。
    • 为了描述进程间的家族关系,通常还设有父进程标识和子进程标识,以表示进程间的家族关系。
    • 此外,还设有用户名或用户标识号表示该进程属于哪个用户。
  • 2.处理机状态
    • 处理机状态信息主要由处理机的各个寄存器内的信息组成。 进程运行时的许多信息均存放在处理机的各种寄存器中。其中 程序状态字(PSW) 是相当重要的,处理机根据程序状态寄存器中的PSW来控制程序的运行。
  • 3.进程调度信息
    • 进程状态: 标识进程的当前状态(就绪、运行、阻塞),作为进程调度的依据。
    • 进程优先级: 表示进程获得处理机的优先程度。
    • 为进程调度算法提供依据的其他信息:例如,进程等待时间、进程已经获得处理器的总时间和进程占用内存的时间等。
    • 事件: 是指进程由某一状态转变为另一状态所等待发生的事件。(比如等待I/O释放)
  • 4.进程控制信息
    • 程序和数据地址: 是指组成进程的程序和数据所在内存或外存中的首地址,以便在调度该进程时能从其PCB中找到相应的程序和数据。
    • 进程同步和通信机制: 指实现进程同步和通信时所采取的机制,如消息队列指针和信号量等,他们可以全部或部分存在PCB中。
    • 资源清单: 列出了进程所需的全部资源 及 已经分配给该进程的资源,但不包括CPU.
    • 链接指针: 它给出了处于同一队列中的下一个PCB的首地址。

5、特殊进程

5.1 僵尸进程(Zombie )(面试重点)

5.1.1、定义

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

5.1.2、产生背景
  • 每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息:

    • 包括进程号the process ID,
    • 退出状态the termination status of the process,
    • 运行时间the amount of CPU time taken by the process等)。
  • 直到父进程通过wait / waitpid来取时才释放。但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程.

5.1.3、危害场景
  • 有个进程,它定期的产生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程。
  • 僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程
  • 方法:kill发送SIGTERM或者SIGKILL信号杀死该父进程

Ubuntu命令找到系统僵尸进程并杀死:
1、 ps aux | grep 'Z' 来找到僵尸进程
2、pstree -p -s PID来寻找编号为PID进程也就是僵尸进程的父级进程

通常,僵尸进程的父进程是gnome-session,终结他会注销系统

5.1.4、僵尸进程测试程序
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>

int main()
{
    pid_t pid;
    pid = fork();
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    else if (pid == 0)
    {
        printf("I am child process.I am exiting.\n");
        exit(0);
    }
    printf("I am father process.I will sleep two seconds\n");
    //等待子进程先退出
    sleep(2);
    //输出进程信息
    system("ps -o pid,ppid,state,tty,command");
    printf("father process is exiting.\n");
    return 0;
}

gcc zombie.c -o test1
./test1

在这里插入图片描述

5.1.5、僵尸进程解决办法
(1)通过信号机制

子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。测试程序如下所示:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>

static void sig_child(int signo);

int main()
{
    pid_t pid;
    //创建捕捉子进程退出信号
    signal(SIGCHLD,sig_child);
    pid = fork();
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    else if (pid == 0)
    {
        printf("I am child process,pid id %d.I am exiting.\n",getpid());
        exit(0);
    }
    printf("I am father process.I will sleep two seconds\n");
    //等待子进程先退出
    sleep(2);
    //输出进程信息
    system("ps -o pid,ppid,state,tty,command");
    printf("father process is exiting.\n");
    return 0;
}

static void sig_child(int signo)
{
     pid_t        pid;
     int        stat;
     //处理僵尸进程
     while ((pid = waitpid(-1, &stat, WNOHANG)) >0)
            printf("child %d terminated.\n", pid);
}

在这里插入图片描述

(2)fork两次

原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int main()
{
    pid_t  pid;
    //创建第一个子进程
    pid = fork();
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    //第一个子进程
    else if (pid == 0)
    {
        //子进程再创建子进程
        printf("I am the first child process.pid:%d\tppid:%d\n",getpid(),getppid());
        pid = fork();
        if (pid < 0)
        {
            perror("fork error:");
            exit(1);
        }
        //第一个子进程退出
        else if (pid >0)
        {
            printf("first procee is exited.\n");
            exit(0);
        }
        //第二个子进程
        //睡眠3s保证第一个子进程退出,这样第二个子进程的父亲就是init进程里
        sleep(3);
        printf("I am the second child process.pid: %d\tppid:%d\n",getpid(),getppid());
        exit(0);
    }
    //父进程处理第一个子进程退出
    if (waitpid(pid, NULL, 0) != pid)
    {
        perror("waitepid error:");
        exit(1);
    }
    exit(0);
    return 0;
}

在这里插入图片描述

5.2 孤儿进程(不会有什么危害)

5.2.1、定义

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

5.2.2、孤儿进程的回收(init进程)

孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

注意:

  • 任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。
    • 如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。
    • 如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。
5.2.3、孤儿进程测试程序
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>

int main()
{
    pid_t pid;
    //创建一个进程
    pid = fork();
    //创建失败
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    //子进程
    if (pid == 0)
    {
        printf("I am the child process.\n");
        //输出进程ID和父进程ID
        printf("pid: %d\tppid:%d\n",getpid(),getppid());
        printf("I will sleep five seconds.\n");
        //睡眠5s,保证父进程先退出
        sleep(5);
        printf("pid: %d\tppid:%d\n",getpid(),getppid());
        printf("child process is exited.\n");
    }
    //父进程
    else
    {
        printf("I am father process.\n");
        //父进程睡眠1s,保证子进程输出进程id
        sleep(1);
        printf("father process is  exited.\n");
    }
    return 0;
}

在这里插入图片描述

5.3 守护进程(daemon)(面试要点)

5.3.1、定义

Linux Daemon(守护进程)是运行在后台的一种特殊进程,并且不被任何终端产生的终端信息所打断。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。

注意:
一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。

5.3.2、作用

Linux 服务器在启动时也需要启动很多系统服务,它们向本地或网络用户提供了 Linux 的系统功能接口,直接面向应用程序和用户,而提供这些服务的程序就是由运行在后台的守护进程来执行的。
Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括:

  • 系统日志进程syslogd、
  • web服务器httpd、
  • 邮件服务器sendmail
  • 数据库服务器mysqld等。
5.3.3、分类(部分)

1、 stand alone类型的守护进程

所谓 stand alone,指的是可独立启动的守护进程,这种类型的守护进程以下 2 大特点:

  • 可以自动自动运行,而不需要利用系统其它机制来管理;
  • 启动之后会一直占用内存和系统资源。

基于以上 2 个特点,这种守护进程就拥有一个非常突出的优点,即响应最快。stand alone 守护进程非常多,比如常见的 apache、mysql 等。

2、 xinetd类型的守护进程

  • 由一个统一的 stand alone 守护进程来负责唤起,这个特殊的守护进程被称为 super daemon。
  • 之所以会引入这种机制,是因为 stand alone 会一直占用内存和系统资源,因此有人就提出了按需分配的这种概念。换句话说,当没有客户端要求的时候,xinetd类型的守护进程属于未启动状态,待有客户端要求服务时,super daemon 才会唤醒指定的 xinetd 守护进程。
  • xinetd 类型守护进程的缺点就是不能及时相应,但是优先很明显,
    • 其一,由于 super daemon 负责唤醒各项服务,因此可以赋予super daemon安全管控的机制,这就类似网络防火墙的功能了;
    • 其二,也是它的设置初衷,即客户端的联机结束后就关闭,不会一直占用系统资源。
5.3.3、创建

1、背景知识:

  • 进程组:一个或多个进程的集合,进程组由进程组ID标识,进程组长的进程ID和进程组ID一致,并且进程组ID不会由于进程组长的退出而受到影响
  • 会话周期:一个或多个进程组的集合,比如用户从登陆到退出,这个期间用户运行的所有进程都属于该会话周期
  • setsid函数:创建一个新会话,并担任该会话组的组长,调用setsid函数的目的:让进程摆脱原会话,原进程组,原终端的控制

2、创建守护进程的过程:

  • 1.创建子进程,父进程退出
    子进程变成孤儿进程,然后由1号init进程收养
  • 2.子进程创建新会话
    调用setsid创建新的会话,摆脱原会话,原进程组,原终端的控制,自己成为新会话的组长
  • 3.将当前目录改为根目录
    正在运行的进程文件系统不能卸载,如果目录要回退,则此时进程不能做到,为了避免这种麻烦,以根目录为当前目录
  • 4.重设文件权限掩码
    子进程的文件权限掩码是复制的父进程的,不重新设置的话,会给子进程使用文件带来诸多麻烦
  • 5.关闭不需要的文件描述符
    子进程的文件描述符也是从父进程复制来的,那些不需要的文件描述符永远不会被守护进程使用,会白白的浪费系统资源,还可能导致文件系统无法结束。
5.3.4、守护进程样例

守护线程每隔100s就向文件写入一句话

//https://www.cnblogs.com/yinbiao/p/11203225.html
#include <iostream>
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<semaphore.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
using namespace std;

#define max_file 65535

int main()
{
    pid_t pc;
    int fd,len;

    char buf[]="this is a demo\n";
    len=strlen(buf);

    pc=fork();//第一步,创建子进程

    if(pc<0)
    {
        cout<<"fork error"<<endl;
        exit(1);
    }
    else if(pc>0)
    {
        exit(0);//第二步,父进程退出
    }
    else
    {
        setsid();//第三步,创建新会话

        chdir("/");//第四步,将当前目录改为根目录

        umask(0);//第五步,重新设置文件权限掩码

        for(int i=0; i<max_file; i++)
        {
            close(i);//第六步,关闭不需要的文件描述符
        }

        while(1)
        {
            if((fd=open("/tmp/dameo.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0)
            {

                cout<<"open erro"<<endl;
                exit(1);
            }
            write(fd,buf,len+1);

            close(fd);

            sleep(100);//每隔100s输出一句话到文件
        }
    }
}

6、进程监控(linux指令)(面试要点)

参考

6.1 使用ps命令监控系统进程(重要)

  • ps -aux 查看所有的在内存中的进程信息
  • ps -ajx 查看进程组相关信息,可以追踪进程之间的血缘关系
  • ps -ef 线城市所有进程信息,并显示程序间的关系
  • ps -u username 显示指定用户username信息

6.2 使用top命令监控系统进程(重要)

实时显示系统中各个进程的资源占用情况,按"q"退出top命令

  • top -H -p pid显示对应pid的所有线程资源使用情况
    部分显示参数
  • load average 表示系统最近1min,5min,15min的平均负载,越大表示负载越来越小
  • %MEM物理内存占用比
  • Cpu(s)和%cpu
    • Cpu(s)表示的是所有用户进程占用整个cpu的平均值
    • %CPU显示的是进程占用一个核的百分比,而不是整个cpu(8核)的百分比,有时候可能大于100,那是因为该进程启用了多线程占用了多个核心,所以有时候我们看该值得时候会超过100%,但不会超过总核数*100。

6.3 使用lsof命令监控系统进程

  • lsof -i :8600 查看8600端口的运行情况
  • lsof -u username 查看username打开的文件
  • lsof -c string 查看包含指定字符的进程所打开的文件

6.4 kill

kill -9 pid杀掉指定进程

参考

1、https://www.cnblogs.com/Anker/p/3271773.html
2、https://www.cnblogs.com/shijiaqi1066/p/3836017.html
3、https://zhuanlan.zhihu.com/p/145245419
4、https://www.cnblogs.com/mickole/p/3188321.html
5、https://www.cnblogs.com/yinbiao/p/11203225.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值