• Linux进程概念详解


    一、进程的基本概念

    进程(process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

    简而言之:进程 = 可执行程序 + 该进程对应的内核数据结构

    二、进程的描述-PCB

    PCB(process control block)即进程控制块,进程信息就是被进程控制块的数据结构中,可以理解为进程属性的集合。在Linux中描述进程的结构体叫做task_struct,task_struct是PCB的一种,同时它也是是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息

    task_struct的内容:

    • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
    • 状态: 任务状态,退出代码,退出信号等。
    • 优先级: 相对于其他进程的优先级。
    • 程序计数器: 程序中即将被执行的下一条指令的地址。
    • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
    • 上下文数据: 进程执行时处理器的寄存器中的数据。
    • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
    • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
    • 其他信息

    【注意】以上内容都可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。

    三、对进程的理解

    1. 查看进程

    • 进程的信息可以通过 /proc 系统文件夹查看

    • 大多数进程信息同样可以使用top和ps这些用户级工具来获取
     ps axj
    
    • 1

    2. 通过系统调用获取进程标识符

    • 进程id(PID),获取PID的系统函数:getpid()
    • 父进程id(PPID),获取PPID的系统函数:getppid()

    利用man手册查看getpid/getppid


    举个例子:

    #include 
    #include 
    #include 
    int main()
    {
    	printf("pid: %d\n", getpid());
    	printf("ppid: %d\n", getppid());
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3. 通过系统调用(fork)创建进程

    使用man 手册认识fork:

    由此我们可以对fork产生简单的认识:
    Linux下系统调用可以用于创建子进程:fork(),当一个进程正在运行的时候,使用了fork()函数之后就会创建另一个进程。与一般函数不同的是,fork()函数会有两次返回值,一次返回给父进程(该返回值是子进程的PID(Process ID)),第二次返回是给子进程,其返回值为0。所以在调用该函数以后,我们需要通过返回值来判断当前的代码时父进程还是子进程在运行:

    返回值大于0 -> 父进程在运行
    返回值等于0 -> 子进程在运行
    返回值小于0 -> 函数系统调用出错

    【注意】通常系统调用出错的原因有两个:①已存在的系统进程已经太多;②该实际用户ID的进程总数已经超过了限制。

    一个简单的例子:

    #include
    #include
    #include
    
    
    int main()
    {
        pid_t id = fork(); //子进程返回0,父进程返回 >0 。
    
        if(id == 0)
        {
            while(1)
            {
                printf("我是子进程,我的pid是:%d,我的父进程是:%d\n", getpid(), getppid());
                sleep(1);
            }
        }
        else 
        {
            while(1)
            {
                printf("我是父进程,我的pid是:%d,我的父进程是:%d\n", getpid(), getppid());
                sleep(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

    结果:

    【注意】

    • 该示例代码中,使用了fork()函数了之后,下面通过pid的返回值来判断是父进程还是子进程在被调用,或者是系统调用失败(pid < 0 时)。其父子进程的运行顺序并不是一定的,这个系统的调度策略有关系。
    • 使用同一个id却产生了两种不同的结果,其中的原因在我们对进程地址空间了解之后自然就会明白。系统调用fork函数之后,父子进程会共享代码,一般都会执行后续代码,但是对于数据会各自开辟空间,各自一份,互不影响。

    四、进程状态

    1、进程状态转换

    基本状态:

    • 就绪状态(ready):进程已经分配除了CPU之外的所有必要资源,只要再获得CPU资源就可以立即执行。
    • 执行状态(running):进程已经获得CPU资源,程序正在执行。
    • 阻塞状态(block):进程等待某种资源(非CPU资源),资源没有就绪时,进程需要在该资源的等待队列中排队,此时程序的代码没有运行,进程所处的状态就叫做阻塞状态。
    • 挂起状态(hang):当内存不足的时候,并且短期内程序不会被调度,如果程序的代码和数据依旧在内存中就会浪费内存空间,操作系统就会把该进程的代码和数据置换到磁盘中,这个过程称为挂起状态。

    2. Linux内核源代码定义的进程状态

    为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。下面的状态在kernel源代码里定义:

    /*
    * The task state array is a strange "bitmap" of
    * reasons to sleep. Thus "running" is zero, and
    * you can test for combinations of others with
    * simple bit tests.
    */
    static const char * const task_state_array[] = {
    "R (running)", /* 0 */
    "S (sleeping)", /* 1 */
    "D (disk sleep)", /* 2 */
    "T (stopped)", /* 4 */
    "t (tracing stop)", /* 8 */
    "X (dead)", /* 16 */
    "Z (zombie)", /* 32 */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
    • S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
    • D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
    • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
    • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

    3. 进程状态的查看命令

    ps aux / ps axj
    
    • 1

    4. 僵尸状态

    僵尸状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程
    僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

    一个变僵尸进程的例子:

    #include
    #include 
    #include 
    
    void test_zombies_process()
    {
        pid_t id = fork();
        
          if (id == 0)
          {
             int cnt = 5;
             while(cnt--)
             {
                printf("我是子进程,我还剩下%d秒\n", cnt);
                sleep(1);
             }
             printf("我是子进程,我变成僵尸了\n");
             exit(0);
          }
          else
          {
              while(1)
              {
              printf("我是父进程\n");
              sleep(1);
              }
          }
    }
    int main()
    {
    	test_zombies_process();
    	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
    • 33


    僵尸进程危害:

    • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态!
    • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护!
    • 当一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!

    5. 孤儿进程

    在操作系统领域中,孤儿进程指的是在其父进程执行完成或被终止后仍继续运行的一类进程。这些孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

    一个孤儿进程的例子:

    void test_orphan_process()
    {
        pid_t id = fork();
          
        if(id == 0)
        {
            while(1)
            {
                printf("我是子进程\n");
                sleep(1);
            }
        }
        else
        {
          int cnt = :wq5;
            while(cnt--)
            {
                printf("我是父进程,我还剩%d秒\n", cnt);
                sleep(1);
            }
            sleep(3);
            exit(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


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

    五、进程优先级

    1. 进程优先级的概念

    由于系统中进程数量众多,而CPU资源比较少甚至只有一个,进程之间需要竞争来使用CPU。这时让一个比较重要、需要优先执行的进程去和其他进程竞争,显然是不合理的。为了更合理的分配CPU资源, 就有了进程优先级。优先级高的进程有优先执行的权利。此外,优先级还影响分配给进程的时间片长短。 重要的进程,应该分配多一些cpu时间片,好让其尽快完成任务。所有的进程都会有机会运行,但优先级高的进程会获取更多的cpu执行时间。配置进程优先级对多任务环境的Linux很有用,可以改善系统性能

    2. 优先级的查看

    在linux系统中,用ps –l命令则会类似输出以下几个内容:

    我们很容易会注意到其中的几个信息:

    UID : 代表执行者的身份
    PID : 代表这个进程的代号
    PPID :代表这个进程是由哪个进程发展衍生而来的,即父进程的代号
    PRI :代表这个进程可被执行的优先级,其值越小越早被执行
    NI :代表这个进程的nice值

    3. PRI 和 NI

    PRI,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高而NI就是我们所说的nice值,其表示进程可被执行的优先级的修正数值
    PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice 。这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。所以,调整进程优先级,在Linux下,就是调整进程nice值nice 其取值范围是 -20 至 19 ,一共40个级别。

    【注意】
    需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。可以理解nice值是进程优先级的修正修正数据。

    4. 修改进程的优先级

    用top命令更改已存在进程的nice:

    • 先执行 top 指令
    • 进入top后按 【r】 ,输入进程PID,输入nice值

    六、进程相关的其他概念

    • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
    • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
    • 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。
    • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
  • 相关阅读:
    python学习:split()分割字符串和join()合并字符串
    Mysql数据库SQL语句与管理
    Android的JSON解析(上)
    写一个自己的编码风格校验工具
    C语言学习之路
    python+pytest接口自动化(4)-requests发送get请求
    竞赛选题 深度学习动物识别 - 卷积神经网络 机器视觉 图像识别
    【CPP】数组名与指针
    CF - D1/2. Burenka and Traditions (DP,异或,思维)
    Shell基础练习2
  • 原文地址:https://blog.csdn.net/qq_61635026/article/details/126324129