Linux进程PCB|创建|优先级

前言

大家好,我是jiantaoyab,这篇文章给大家介绍进程。

进程基本概念

所有启动程序的过程,本质上都是在创建进程,程序是一个可执行的文件,而进程(process)是一个执行中的程序实例。利用分时技术,在 Linux操作系统上同时可以运行多个进程。分时技术的基本原理是把 CPU 的运行时间划分成一个个规定长度的时间片,让每个进程在一个时间片内运行。当进程的时间片用完时系统就利用调度程序切换到另一个进程去运行。因此实际上对于具有单个 CPU 的机器来说某一时刻只能运行一个进程。但由于每个进程运行的时间片很短,所以表面看来好象所有进程在同时运行着。系统最大进程(任务)数为 64。

进程由可执行的指令代码、数据和堆栈区组成。进程中的代码和数据部分分别对应一个执行文件中的代码段、数据段。每个进程只能执行自己的代码和访问自己的数据及堆栈区。进程之间相互之间的通信需要通过系统调用来进行。对于只有一个 CPU 的系统,在某一时刻只能有一个进程正在运行。内核通过调度程序分时调度各个进程运行当一个程序被加载到内存中运行,那么内存中的那个数据就被称为进程,所有在系统上运行的程序都会以进程的形式存在。

在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发,用户在访问硬件的时候,os不信任任何用户,用户使用c/c++的库函数,库函数里面又调用到系统调用接口(函数)。

进程PCB

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

task_ struct内容分类

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

任务数据结构在头文件 include/linux/sched.h中

struct task_struct {
  long state //任务的运行状态(-1 不可运行,0 可运行(就绪),>0 已停止)。
  long counter // 任务运行时间计数(递减)(滴答数),运行时间片。
  long priority // 运行优先数。任务开始运行时 counter=priority,越大运行越长。
  long signal // 信号。是位图,每个比特位代表一种信号,信号值=位偏移值+1。
  struct sigaction sigaction[32] // 信号执行属性结构,对应信号将要执行的操作和标志信息。
  long blocked // 进程信号屏蔽码(对应信号位图)。
  int exit_code // 任务执行停止的退出码,其父进程会取。
  unsigned long start_code // 代码段地址。
  unsigned long end_code // 代码长度(字节数)。
  unsigned long end_data // 代码长度 + 数据长度(字节数)。
  unsigned long brk // 总长度(字节数)。
  unsigned long start_stack // 堆栈段地址。
  long pid // 进程标识号(进程号)。
  long father // 父进程号。
  long pgrp // 父进程组号。
  long session // 会话号。
  long leader // 会话首领。
  unsigned short uid // 用户标识号(用户 id)。
  unsigned short euid // 有效用户 id。
  unsigned short suid // 保存的用户 id。
  unsigned short gid // 组标识号(组 id)。
  unsigned short egid // 有效组 id。
  unsigned short sgid // 保存的组 id。
  long alarm // 报警定时值(滴答数)。
  long utime // 用户态运行时间(滴答数)。
  long stime // 系统态运行时间(滴答数)。
  long cutime // 子进程用户态运行时间。
  long cstime // 子进程系统态运行时间。
  long start_time // 进程开始运行时刻。
  unsigned short used_math // 标志:是否使用了协处理器。
  int tty // 进程使用 tty 的子设备号。-1 表示没有使用。
  unsigned short umask // 文件创建属性屏蔽位。
  struct m_inode * pwd // 当前工作目录 i 节点结构。
  struct m_inode * root // 根目录 i 节点结构。
  struct m_inode * executable // 执行文件 i 节点结构。
  unsigned long close_on_exec // 执行时关闭文件句柄位图标志。(参见 include/fcntl.h)
  struct file * filp[NR_OPEN] // 文件结构指针表,最多 32 项。表项号即是文件描述符的值。
  struct desc_struct ldt[3] // 任务局部描述符表。0-空,1-代码段 cs,2-数据和堆栈段 ds&ss。
  struct tss_struct tss // 进程的任务状态段信息结构。
};

可以看到task_struct中含有很多信息,我们这里重点理解进程上下文

当一个进程在执行时,CPU 的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换(switch)至另一个进程时,它就需要保存当前进程的所有状态,也即保存当前进程的上下文,以便在再次执行该进程时,能够恢复到切换时的状态执行下去。

在 Linux 中,当前进程上下文均保存在进程的任务数据结构中。在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中断服务结束时能恢复被中断进程的执行。

那么在抢占式多任务处理中,进程被抢占时,哪些运行环境需要被保存下来?

  1. 所有cpu寄存器中的内容
  2. 页表指针-- 程序切换时会将页表起始地址加载到寄存器中
  3. 程序计数器–下一步程序要执行的指令地址

创建进程

对于 Linux 0.11 内核来讲,系统最多可有 64 个进程同时存在。除了第一个进程是“手工”建立以外,其余的都是进程使用系统调用 fork 创建的新进程,被创建的进程称为子进程(child process),创建者,则称为父进程(parent process)。内核程序使用进程标识号(process ID,pid)来标识每个进程。进程由可执行的指令代码、数据和堆栈区组成。进程中的代码和数据部分分别对应一个执行文件中的代码段、数据段。

每个进程只能执行自己的代码和访问自己的数据及堆栈区。进程之间相互之间的通信需要通过系统调用来进行。对于只有一个 CPU 的系统,在某一时刻只能有一个进程正在运行。内核通过调度程序分时调度各个进程运行。

Linux 系统中,一个进程可以在内核态(kernel mode)或用户态(user mode)下执行,因此,Linux内核堆栈和用户堆栈是分开的。

用户堆栈用于进程在用户态下临时保存调用函数的参数、局部变量等数据。内核堆栈则含有内核程序执行函数调用时的信息。

我们可以通过fork()手动创建子进程,进程调用fork()之后,内核分配新的内存块和内核数据结构给子进程,将父进程部分数据结构内容拷贝至子进程,添加子进程到系统进程列表当中

用man fork查看fork的具体用法可以知道。父进程和子进程的代码是共享的,代码是不可以修改的,父子代码只有一份,默认的情况下数据也是共享的,环境变量默认会继承于父进程,与父进程相同,当数据需要修改的时候,通过写实拷贝修改。

image-20240331153215087

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
  int ret = fork();
  if(ret < 0)
  {
    perror("fork");
    return 1;
  }
  /*
  	这里父进程和子进程调用的先后顺序是不确定的,由调度器决定
  */
  else if(ret == 0)
  { //child
    printf("I am child : %d!, ret: %d\n", getpid(), ret);
  }else
  { //father
    printf("I am father : %d!, ret: %d\n", getpid(), ret);
  }
  sleep(1);
  return 0;
}

fork()有2个返回值,给子进程返回0,给父进程返回子进程的pid。

image-20240331153329439

查看进程

ls /proc 查看进程的信息

image-20240331194220935

ps aux | grep 可执行文件 | gerp -v grep

image-20240331194311497

可以看到我们的进程ID号1750,这里显示的是S+,S表示处于可中断睡眠状态,+表示在前台运行,我这里执行的test程序在不断的打印hello,为什么不是R状态呢?

我调用cout向终端输出内容,是一种向慢设备输出的一个过程,在CPU看来这个操作非常的慢,大部分时间都在等待。

向这个进程发送SIGSTOP信号,可以看到我们的进程处于T状态

image-20240331195157830

再向这个进程发送SIGCONT信号,可以看到我S的+号没了,此时我在运行test的终端ctrl + c杀不掉这个进程了,因为这个进程被放到后台去了,此时可以通过Kill -9 10750来杀这个进程。

image-20240331195258223

我们平时在执行可执行文件的时候,加上&可执行程序也会放到后台

进程状态

一个进程在其生存期内,可处于一组不同的状态下,称为进程状态。进程状态保存在进程任务结构的 state 字段中。当进程正在等待系统中的资源而处于等待状态时,则称其处于睡眠等待状态。在 Linux 系统中,睡眠等待状态被分为可中断的和不可中断的等待状态。

#define TASK_RUNNING 0 // 进程正在运行或已准备就绪。
#define TASK_INTERRUPTIBLE 1 // 进程处于可中断等待状态。
#define TASK_UNINTERRUPTIBLE 2 // 进程处于不可中断等待状态,主要用于 I/O 操作等待。
#define TASK_ZOMBIE 3 // 进程处于僵死状态,已停止运行,但父进程还没发信号。
#define TASK_STOPPED 4 // 进程已停止。

image-20240331161014074

运行状态(TASK_RUNNING)R

当进程正在被 CPU 执行,或已经准备就绪随时可由调度程序执行(在运行队列中),则称该进程为处于运行状态。

进程可以在内核态运行,也可以在用户态运行。当系统资源已经可用时,进程就被唤醒而进入准备运行状态,该状态称为就绪态。

可中断睡眠状态(TASK_INTERRUPTIBLE)S

当进程处于可中断等待状态时(通常进程在等待事件完成),系统不会调度该进行执行。当系统产生一个中断或者释放了进程正在等待的资源,或者进程收到一个信号,都可以唤醒进程转换到就绪状态(运行状态)。

不可中断睡眠状态(TASK_UNINTERRUPTIBLE)D

与可中断睡眠状态类似。但处于该状态的进程只有被使用 wake_up()函数明确唤醒时才能转换到可运行的就绪状态,在这个状态的进程通常会等待IO的结束。

暂停状态(TASK_STOPPED)T

当进程收到信号 SIGSTOP、SIGTSTP、SIGTTIN 或 SIGTTOU 时就会进入暂停状态。可向其发送SIGCONT 信号让进程转换到可运行状态。在 Linux 0.11 中,还未实现对该状态的转换处理。处于该状态的进程将被作为进程终止来处理。

僵死状态(TASK_ZOMBIE) Z

当进程已停止运行,但其父进程还没有询问其状态时,则称该进程处于僵死状态。

我们重点看看僵尸状态,为什么要有僵尸状态呢?

当进程退出的时候,先进入僵尸状态,这个时候进行退出的相关信息还保留在task_struct中,方便我们查看。

僵尸进程

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

孤儿进程

父进程先退出,子进程就称之为“孤儿进程”,孤儿进程是运行在后台的孤儿进程被1号init进程领养,被init进程回收。

进程优先级

cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能

查看优先级

image-20240401170638625

  • UID : 执行者的身份(系统是根据UID来确定用户是谁)
  • PID : 进程的代号
  • PPID :父进程
  • PRI :进程可被执行的优先级,其值越小越早被执行
  • NI :进程的nice值

这里的nice值是什么呢?

NI表示进程可被执行的优先级的修正数值,PRI的值越小越快被执行,一般PRI表示为 PRI = PRI(80) + nice,nice值的范围是[-20, 19]。

可以通过top命令来设置优先级,我运行了一个无限循环打印的程序。

image-20240401220714925

按一下r,输入值

image-20240401221143663
在这里插入图片描述

结果显示,可以到test的PRI变成90了。

image-20240401221844447

其他的几个概念

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

  • 24
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值