★ Linux ★ 进程(上)

发布于:2025-03-20 ⋅ 阅读:(22) ⋅ 点赞:(0)

Ciallo~(∠・ω< )⌒☆ ~ 今天,我将和大家一起学习 linux 进程~

​❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️

澄岚主页:椎名澄嵐-CSDN博客

Linux专栏:https://blog.csdn.net/2302_80328146/category_12815302.html?spm=1001.2014.3001.5482

​❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️


目录

壹  基本概念与基本操作

1.1 task_ struct

1.2 查看进程

1.3 创建进程

贰  进程状态

2.1 进程状态的概念

2.2 运行 阻塞 挂起

2.3 进程状态查看

2.4 僵尸进程

2.5 孤儿进程

叁  进程优先级

3.1 基本概念

3.2 查看系统进程

3.3 PRI and NI

3.4 查看进程优先级的命令

3.5 竞争、独立、并行、并发

肆  进程切换

伍  Linux2.6内核进程O(1)调度队列

5.1 优先级

5.2 活动队列

5.3 过期队列

5.4 active指针和expired指针

~完~


壹  基本概念与基本操作

进程是程序的一个执行实例,正在执行的程序等,或者说担当分配系统资源(CPU时间,内存)的实体。

进程信息(PCB)被放在一个叫做进程控制块的数据结构中,Linux操作系统下的PCB是: task_struct。进程的所有属性都可以直接或间接的通过task_struct找到。

进程 = PCB(struct_task)+ 自己的代码和数据 ~

对进程的管理就变成了对链表的增删查改 ~

1.1 task_ struct

task_ struct 中包含:

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

1.2 查看进程

历史上我们执行的所有指令,工具,自己的程序,运行起来都是进程~

我们可以通过系统调用getpid获取进程标示符~

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
    while(1)
    {
        sleep(1);
        printf("我是一个进程~,我的pid: %d\n", getpid());
    }
}   

每次执行的进程值都不同~

终止进程可以使用 ctrl + C 或 kill -9~ 

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

进程结束后此进程的pid为名的文件夹会被销毁 ~

因为cwd默认当前目录,所以fopen创建文件时会在当前目录下生成~

更改目录可以使用 chdir (也是系统调用)~

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/types.h>
  4 int main()
  5 {
  6     chdir("/home/zmzz");
  7     fopen("Ciallo.txt","a");
  8     while(1)
  9     {
 10         sleep(1);
 11         printf("我是一个进程~,我的pid: %d,我的父进程id: %d\n", getpid(), getppid());
 12     }
 13 }   

getppid() 可以查看父进程,父进程不会变(bash命令行解释器本质是一个进程~)

每有一个用户就会有一个bash

1.3 创建进程

fork可以用来创建子进程~

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/types.h>
  4 int main()
  5 {
  6     printf("父进程开始运行:pid= %d\n", getpid());
  7     fork();
  8     printf("进程开始运行:pid= %d\n", getpid()); 
  9 }      

fork有两个返回值~

一个父进程可以有多个子进程,是1:n的关系,父进程也需要知道子进程的pid进行控制,所以fork会给父子不同的返回值~

在fork函数中return id;语句执行前子进程已经被创建和调度了~所以fork会返回两次~

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
    printf("父进程开始运行:pid= %d\n", getpid());
    pid_t id  = fork();
    if (id < 0)
    {
        perror("fork fail~");
        return 1;
    }
    else if (id == 0)
    {
        // child
        while(1)
        {
             sleep(1);
             printf("我是一个子进程~,我的pid: %d,我的父进程id: %d\n", getpid(), getppid());
        }
    }
    else
    {
        // father
        while(1)
        {
             sleep(1);
             printf("我是一个父进程~,我的pid: %d,我的父进程id: %d\n", getpid(), getppid());
        }
    }
    printf("进程开始运行:pid= %d\n", getpid());
}

进程具有独立性,父子任何一方进行修改数据,OS会把修改的数据在底层拷贝一份,让目标进程修改这个拷贝~这个拷贝叫做采用写时拷贝~

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

int gval = 100;

int main()
{
    printf("父进程开始运行:pid= %d\n", getpid());
    pid_t id  = fork();
    if (id < 0)
    {
        perror("fork fail~");
        return 1;
    }
    else if (id == 0)
    {
        printf("我是一个子进程~,我的pid: %d,我的父进程id: %d, gval: %d\n", getpid(), getppid(), gval);
        sleep(5);
        // child
        while(1)
        {
             sleep(1);
             printf("子进程修改变量:%d->%d\n", gval, gval+10);
             gval += 10; // 修改
             printf("我是一个子进程~,我的pid: %d,我的父进程id: %d\n", getpid(), getppid());
        }
    }
    else
    {
        // father
        while(1)
        {
             sleep(1);
             printf("我是一个父进程~,我的pid: %d,我的父进程id: %d, gval: %d\n", getpid(), getppid(), gval);
        }
    }
    printf("进程开始运行:pid= %d\n", getpid());
}


贰  进程状态

2.1 进程状态的概念

进程状态本质上就是task_struct内的一个整数。不同的整数代表不同的状态。

2.2 运行 阻塞 挂起

运行:进程在调度队列中,进程的状态都是running。

阻塞:等待某种设备或资源就绪。(键盘,显示器,网卡,磁盘,摄像头,话筒等)

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 */
};
  • R运行状态(running):并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里
  • S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠或者浅睡眠(interruptible sleep))。
  • D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
  • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行
  • t停止状态(tracing stopped):  被debug,断点,进程被暂停了。
  • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态

2.3 进程状态查看

ps aux / ps axj 命令
  • a:显示一个终端所有的进程,包括其他用户的进程。
  • x:显示没有控制终端的进程,例如后台运行的守护进程。
  • j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息
  • u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等

2.4 僵尸进程

僵死状态(Zombies)是一个比较特殊的状态。

创建子进程的目的是为了帮父进程完成某种事情的,父进程需要知道子进程结果相关的信息。此信息会被存在task_struct中。如果父进程一直不回收子进程的退出信息,那么Z状态的PCB会一直存在,会引起内存泄漏问题。

僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int count = 5;
        while(count)
        {
            printf("我是子进程,我正在运行:%d\n", count);
            sleep(1);
            count--;
        }
    }
    else
    {
        //parent
        while(1)
        {
            printf("我是父进程,正在运行...\n");
            sleep(1);
        }
    }
    return 0;
}


// while :; do ps ajx | head -1; ps ajx | grep myprocess; sleep 1; done

2.5 孤儿进程

父进程先退出,子进程被1号systemd(操作系统)进程领养,这个子进程就称之为“孤儿进程”。

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        while(1)
        {
            printf("我是一个子进程, pid = %d, ppid = %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else
    {
        // father
        int cnt = 5;
        while(cnt)
        {
            printf("我是一个父进程, pid = %d, ppid = %d\n", getpid(), getppid());
            cnt--;
            sleep(1);
        }
    }
    return 0;
}

// while :; do ps ajx | head -1 && ps ajx | grep myprocess;sleep 1;done

 1号进程:

变成孤儿进程后,此进程会成为后台进程,ctrlC不能终止,需使用:

kill -9 此进程pid

叁  进程优先级

3.1 基本概念

进程优先级就是进程得到CPU资源的先后顺序~ 

CPU资源稀缺,所以导致要通过优先级确认谁先谁后的问题~

优先级是能得到资源,谁先谁后的问题。而权限是能不能得到资源的问题~

优先级也是一种数字, int, 值越低,优先级越高,反正,优先级越低~ 基于时间片的分时操作系统,考虑公平性,所以优先级可能变化,但变化幅度不会太大~ 

3.2 查看系统进程

ps -al | head -1 && ps -al | grep myprocess

• UID : 代表执行者的身份


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

 top r 10命令后:

3.3 PRI and NI

PRI即进程的优先级,或者说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高~
nice值表示进程可被执行的优先级的修正数值
所以,调整进程优先级,在Linux下,就是调整进程nice值,PRI=80+NICE值
Linux优先级范围为[60, 99],nice其取值范围是-20至19,一共40个级别。

3.4 查看进程优先级的命令

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

  • top
  • 进入top后按“r”–>输入进程PID–>输入nice值

3.5 竞争、独立、并行、并发

  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
  • 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
  • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

肆  进程切换

比如一个大学生要去当兵,去之前他得向辅导员申请保留学籍,一年之后回来时他又得向辅导员申请恢复学籍,这就相当于一次切换,学校就是CPU,导员是调度器,大学生就是进程,学籍是进程运行的临时数据(CPU内寄存器中的内容,当前进程的上下文数据),保留学籍恢复学籍分别相当于保存和恢复进程上下文数据,当兵就是从CPU上剥离下来了。

当前进程要将自己的进程硬件上下文保存到进程的task_struct中(TSS任务状态段)。


伍  Linux2.6内核进程O(1)调度队列

下图是Linux2.6内核中进程队列的数据结构~

一个CPU拥有一个runqueue, 如果有多个CPU就要考虑进程个数的负载均衡问题~

5.1 优先级

  • 普通优先级:100~139(x - 60 + (140 - 40))
  • 实时优先级:0~99(不关心)

5.2 活动队列

  • 时间片还没有结束的所有进程都按照优先级放在该队列
  • nr_active: 总共有多少个运行状态的进程
  • queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!相当于一个哈希表管理的一个个链式队列
  • 从该结构中,选择一个最合适的进程,过程是怎么的呢?

1. 从0下表开始遍历queue[140]
2. 找到第一个非空队列,该队列必定为优先级最高的队列
3. 拿到选中队列的第一个进程,开始运行,调度完成!
4. 遍历queue[140]时间复杂度是常数!但还是太低效了!

  • bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!

5.3 过期队列

过期队列和活动队列结构一模一样,过期队列上放置的进程,都是时间片耗尽的进程,当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算。

5.4 active指针和expired指针

  • active指针永远指向活动队列
  • expired指针永远指向过期队列

活动队列上的进程会越来越少,过期队列上的进程会越来越多,在合适的时候,只要交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!


~完~