Liunx 操作系统 进程概念(上)

一.进程

1.基本概念与基本操作

客观概念:程序的一次执行实例,正在运行的程序。

从操作系统角度:进程是一个负责和管理系统资源的实体。

Linux认为一个进程等于:

task_struct + 代码 + 数据

其中task_struct相当于进程的身份证 + 档案

里面记录:

  • 谁是这个进程
  • 当前状态
  • 优先级
  • 内存在哪里
  • 打开了哪些文件

2.进程PCB

进程信息被放在⼀个叫做进程控制块的数据结构中,可以理解为进程属性的集合。

操作系统为了管理进程,会给每个进程建立一个结构体。

task_struct 是PCB的一种

3.task_struct

内容分类:

1. 标识符(PID)

作用:区分不同进程。

例如:

复制代码
PID=1001 微信

PID=1002 浏览器

PID=1003 QQ

Linux查看:ps aux

可以看到PID

2. 状态

表示进程当前情况。

Linux常见:

状态 含义
R 运行
S 睡眠
D 不可中断睡眠
T 停止
Z 僵尸

3. 优先级

CPU资源有限。

多个进程竞争CPU。

例如:

复制代码
游戏      优先级高

后台更新  优先级低

调度器根据优先级决定谁先运行。

4. 程序计数器(PC)

保存下一条要执行的指令地址。

例如程序:

复制代码
1 printf("hello");
2 printf("world");
3 return 0;

执行到第二行

CPU中:PC ---> 第二行地址

5. 内存指针

记录进程代码和数据在哪里。

6. 上下文数据

CPU运行进程时:

需要保存:

寄存器数据
程序计数器
栈指针

例如:进程A运行:

复制代码
CPU
 |
寄存器
 |
A的数据

突然切换运行进程B

CPU必须保存A当前状态

以后回来恢复A继续执行

这个保存的信息叫:上下文

类似玩游戏存档。

退出游戏保存:

等级
装备
位置

下次继续。

7. IO状态信息

记录:进程使用的输入输出资源。

例如:打开文件:test.txt

task_struct记录:

打开了哪些文件
使用哪些设备

8. 记账信息

记录资源使用情况。

例如:

CPU运行时间

占用内存大小

运行次数

方便性能统计和资源限制。

进程的组织

Linux中:

所有进程不是乱放的。

而是使用:双向链表组织

结构类似:

复制代码
task_struct

     |
     v

+------+     +------+     +------+
|进程1 |<--->|进程2 |<--->|进程3 |
+------+     +------+     +------+

4.查看进程

1. 通过 /proc 查看进程

Linux里面有一个特殊目录:/proc

它不是普通磁盘文件夹,而是:

内核提供的虚拟文件系统

里面保存了当前系统运行进程的信息。

例如查看:ls /proc

你会看到:

复制代码
1
2
3
500
1000
...

这些数字就是:PID

比如:/proc/1

表示查看PID为1的进程信息。

进入:cd /proc/1

查看:ls

会看到:

复制代码
cmdline
status
cwd
exe
fd
maps
...

2. 使用ps查看进程

ps 是最常用的查看进程命令。

例如:ps

显示:

复制代码
PID   TTY     TIME     CMD
1234  pts/0   00:00    bash

但是默认只显示当前终端进程。

一般使用:ps aux或者ps ajx查看所有进程。

3. 使用top查看进程

top类似Windows任务管理器。

输入:top

显示:

复制代码
PID   USER   CPU   MEM   COMMAND

1     root    0    1%    systemd

2330  whb     0    0%    test

可以实时看到:

  • CPU占用
  • 内存占用
  • 进程状态

退出:按q

5.通过系统调用获取进程标示符

Linux中,每一个进程都有自己的身份信息:

  1. PID(Process ID)
    • 进程编号
    • 唯一标识一个进程
  2. PPID(Parent Process ID)
    • 父进程编号
    • 表示是谁创建了当前进程

Linux中进程不是凭空产生的。

一般父进程创建子进程

例如:

打开终端:

复制代码
systemd
   |
   bash
      |
      ./test

那么bash的PPID = systemd的PID

test的PPID = bash的PID

6.通过系统调用创建进程-fork

fork() 是 Linux 中创建进程的系统调用。

作用:创建一个新的进程

调用fork之前:只有一个进程

父进程 PID=1000

调用fork()之后:

复制代码
          fork()

            |
      ----------------
      |              |
   父进程          子进程
 PID=1000        PID=1001

原来的进程叫:父进程(father)

新创建的进程叫:子进程(child)

fork有两个返回值

很多函数一般一个函数调用,一个返回值。

但是fork特殊。

原因:fork创建了一个新的执行流。

fork执行以后:

实际上有两个进程继续往下运行:

复制代码
          fork()
             |
      ----------------
      |              |
      父进程        子进程

所以两个进程都需要知道fork的结果。

因此fork返回两次。

fork返回值

情况1:返回给父进程

父进程中:ret = 子进程PID

例如:

父进程:PID=3000

创建:子进程PID=3001

父进程得到:ret=3001

也就是:父进程拿到子进程的PID

情况2:返回给子进程

子进程:ret=0

例如:

子进程: PID=3001 ret=0

为什么不是自己的PID?

因为:子进程可以通过:getpid()

得到自己PID。

但是:子进程需要知道:我是不是子进程?

所以规定:fork返回0表示自己是子进程

父子进程代码共享,数据独立

fork之后:

父子共享代码

例如:

复制代码
printf()

代码只有一份。

但是数据各自一份。

例如:

复制代码
int a=10;

fork();

a++;

fork后:

父进程: a=10 a++

子进程: a=10 a++

最终:

父: a=11 子: a=11

互不影响。

二.进程状态

1.进程状态

1. R运行状态(Running)

表示:进程正在运行,或者等待运行。

注意:

R不一定代表正在占用CPU。

因为CPU一次只能运行一个进程。

例如有三个进程:

复制代码
CPU

运行
 |
进程A


运行队列:

进程B
进程C

此时:

  • A正在CPU执行
  • B、C也属于R状态

因为它们:已经准备好了,等待CPU调度。

所以:R = 正在运行 + 等待运行

2. S睡眠状态(Sleeping)

表示:进程正在等待某个事件发生。

例如程序:

复制代码
while(1)
{
    sleep(1);
}

执行:

复制代码
运行
 |
sleep()
 |
等待1秒
 |
唤醒

sleep期间:进程状态:S

因为它没有事情做,让CPU去执行其他程序。


可中断睡眠

S也叫:可中断睡眠(interruptible sleep)

意思:睡眠中的进程可以被信号唤醒。

例如程序:

复制代码
pause();

等待信号:

复制代码
睡眠
 |
收到信号
 |
继续运行

3. D磁盘休眠状态

也叫:不可中断睡眠

表示:进程正在等待IO完成。

例如:

程序读取硬盘:

复制代码
read(file);

过程:

复制代码
进程
 |
请求硬盘数据
 |
等待磁盘
 |
D状态
 |
数据返回
 |
继续运行

为什么叫不可中断?

因为如果磁盘IO过程中被强制打断,可能导致:

  • 数据错误
  • 文件损坏

所以Linux一般不允许随便终止。

4. T停止状态(Stopped)

表示:进程暂停执行。

常见方式:

发送:

复制代码
SIGSTOP

例如:

查看进程:

复制代码
ps aux

暂停:

复制代码
kill -19 PID

进程:

复制代码
运行
 |
T状态

恢复:

发送:

复制代码
SIGCONT

命令:

复制代码
kill -18 PID

状态:

复制代码
T

↓

R

继续运行。

5. X死亡状态(Dead)

表示:进程已经死亡。

但是:这个状态非常短暂。

过程:

复制代码
运行

↓

退出

↓

释放资源

↓

消失

所以使用:

复制代码
ps

通常看不到X状态。

2.进程状态查看 ------ ps命令

作用:查看当前系统中的进程信息。

常用:

复制代码
ps aux

或者:

复制代码
ps axj

ps aux 命令

格式

复制代码
ps aux

三个参数:

  • a
  • u
  • x

1. a 参数

a:显示所有终端中的进程

默认

复制代码
ps

只能查看当前终端自己的进程。

例如:

复制代码
ps

可能:

复制代码
PID    CMD

3000   bash
4000   ./test

ps a

可以看到:

其他用户以及其他终端的进程。

例如:

复制代码
USER   PID   CMD

root     1   systemd

whb   3000   bash

tom   5000   vim

2. x 参数

x:显示没有控制终端的进程

什么叫控制终端?

比如你打开终端:

复制代码
终端窗口

    |
    |
  bash
    |
   程序

这个程序有终端。

但是后台程序:

例如:

复制代码
服务器程序

nginx
mysql
systemd

没有终端。

这些叫:守护进程(daemon)

默认ps看不到。

加:

复制代码
ps x

就可以显示。

3. u 参数

u:以用户为中心显示详细信息

例如:

复制代码
ps u

显示:

复制代码
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND

解释:

字段 含义
USER 进程属于哪个用户
PID 进程ID
%CPU CPU占用比例
%MEM 内存占用比例
VSZ 虚拟内存大小
RSS 实际占用内存
TTY 终端
STAT 进程状态
START 启动时间
TIME CPU使用时间
COMMAND 执行程序

ps axj 命令

格式:

复制代码
ps axj

参数:

  • a
  • x
  • j

j 参数

j:显示进程关系信息

包括:

  • PPID(父进程ID)

  • PGID(进程组ID)

  • SID(会话ID)

例如:

复制代码
PPID    PID    PGID    SID    COMMAND

1      2000    2000   2000    bash

2000   3000    3000   2000    ./test

表示:

复制代码
bash
 |
 |
test

3.僵尸进程

僵尸进程是 Linux 进程状态中比较特殊的一种:

状态:z

正常情况下:

子进程退出:

复制代码
子进程结束
      |
      |
父进程回收
      |
      |
资源释放

但是如果:

复制代码
子进程退出

↓

父进程还在运行

↓

父进程没有调用 wait() 获取子进程退出信息

↓

子进程变成 Z状态

这就是僵尸进程

Linux中进程退出时,并不是立即删除所有信息。

它会保留一部分信息:

例如:

  • 退出码
  • 退出原因
  • PID

这些信息需要告诉父进程。

例如:

子进程:

复制代码
return 5;

系统需要保存:退出状态 = 5

等父进程:

复制代码
wait();

来读取。

wait()作用

父进程调用:

复制代码
wait();

作用:获取子进程退出状态,并释放子进程资源。

僵尸进程的危害

僵尸进程虽然不运行,但是它的进程信息还保存在内核中,占用系统资源。

task_struct本身也是一个结构体。

需要占用内存。

同理:

Linux每个进程都有:struct task_struct

僵尸进程虽然没有运行

但是还存在

所以占用内存。

僵尸进程是一种内存泄漏。

它不是普通程序里的泄漏。

例如C语言:

复制代码
malloc(100);

没有

复制代码
free();

这是用户空间内存泄漏。


僵尸进程:

复制代码
父进程没有回收子进程

↓

内核没有释放task_struct

↓

资源泄漏

属于:进程资源泄漏。

4.孤儿进程

孤儿进程和僵尸进程正好相反。

简单理解:父进程先退出,子进程还在运行,这个子进程就叫孤儿进程。

正常情况下:

复制代码
父进程
   |
   |
子进程

父进程负责:

  • 创建子进程
  • 管理子进程
  • 回收子进程退出状态

但是如果:

父进程提前结束:

复制代码
父进程退出

   X


子进程继续运行

子进程失去了父亲。

所以叫:孤儿进程

孤儿进程被1号进程init/systemd领养。

Linux启动后:

第一个启动的用户空间进程:

PID:1

现代Linux:

一般是:

复制代码
systemd

以前叫:

复制代码
init

结构:

原来:

复制代码
父进程
   |
   |
子进程

父进程退出:

复制代码
systemd(PID=1)

       |
       |
    子进程

此时子进程的PPID

会变成:1

为什么要让systemd领养?

因为:

最终子进程也可能退出。

例如:

复制代码
systemd

   |
   |
孤儿进程

以后孤儿进程退出

那么需要有人调用:

复制代码
wait()

回收它。

这个责任交给:

复制代码
PID=1的systemd

三.进程优先级

1.基本概念

Linux是一个多任务操作系统,系统中同时存在很多进程,但是:

CPU资源有限,一个CPU同一时间只能执行一个进程。

所以Linux需要决定:

哪个进程先运行,哪个进程后运行。

这个决定依据就是:

进程优先级(priority)

1.基本概念

简单理解:CPU分配资源的先后顺序。

例如:

现在有两个进程:

复制代码
进程A:游戏

进程B:后台下载

如果:游戏优先级高:

复制代码
CPU

先运行游戏

再运行下载

如果下载优先级高:

复制代码
CPU

先运行下载

再运行游戏

指定CPU运行(CPU绑定)

Linux还可以:

让某个进程固定运行在某个CPU上。

例如电脑:

复制代码
CPU0
CPU1
CPU2
CPU3

可以设置:

复制代码
普通程序 → CPU0

重要程序 → CPU1

这样减少不同CPU之间切换,提高性能。

2. 查看进程优先级

命令:

复制代码
ps -l

查看:

例如:

复制代码
UID   PID  PPID  PRI  NI  CMD

root  1000  1    80   0   bash

whb   2000 1000  90   5   test

重点字段:

1.UID

表示:进程属于哪个用户

例如:

复制代码
root

表示root用户创建。


2. PID

进程编号:

例如:

复制代码
2000

唯一标识进程。


3. PPID

父进程ID。

表示:当前进程是谁创建的。

例如:

复制代码
PPID=1000

表示:

PID 1000创建了它。


4. PRI(priority)

PRI:

表示:进程实际优先级。

特点数值越小,优先级越高。

例如:

复制代码
进程A PRI=50

进程B PRI=90

CPU:

优先考虑:

复制代码
A

因为:50 < 90


5. NI(nice值)

nice:

表示:对进程优先级进行修正的值。

范围:-20 ~ 19

共40个等级。

2.PRI和NI关系

重点公式:

PRI(new)=PRI(old)+nicePRI(new)=PRI(old)+nice

例如:

原来:

复制代码
PRI=80

设置:

复制代码
nice=5

那么:

复制代码
PRI=80+5

=85

PRI变大:

优先级降低。


如果:

复制代码
nice=-10

那么:

复制代码
PRI=80-10

=70

PRI变小:

优先级升高。

PRI是:

真正参与CPU调度的优先级。

特点:越小越优先

NI是调整PRI的修正值。

不是最终优先级。

关系:

复制代码
PRI = 原始优先级 + NI

所以:NI影响PRI。

3.修改进程优先级

方法1:top

输入:

复制代码
top

进入后:

按:

复制代码
r

输入:

复制代码
PID

再输入:

复制代码
nice值

例如:

复制代码
PID:
2000

nice:
10

修改。


方法2:nice命令

启动程序时设置:

例如:

复制代码
nice -n 5 ./test

表示:

以nice=5运行。


方法3:renice

修改已经运行的进程:

例如:

复制代码
renice 10 -p 2000

修改:

PID=2000的nice值。

系统函数

Linux提供:

获取:

复制代码
getpriority()

设置:

复制代码
setpriority()

例如:

复制代码
int getpriority(int which,int who);

获取进程优先级。

复制代码
int setpriority(int which,int who,int prio);

设置优先级。

4.竞争、独立、并行、并发

1. 竞争性(Competition)

原因:

复制代码
进程很多

CPU很少

例如:

CPU一个

进程100个

大家竞争:CPU资源

所以需要:优先级


2. 独立性(Independence)

多个进程运行:

彼此互不影响。

例如:

复制代码
微信进程

浏览器进程

游戏进程

微信崩溃:不会影响浏览器。

原因:

每个进程有自己的:

  • 地址空间
  • 数据
  • 资源

3. 并行(Parallel)

条件: 多个CPU

例如:

4核CPU:

复制代码
CPU1 → 微信

CPU2 → 浏览器

CPU3 → 游戏

CPU4 → 音乐

多个进程真正同时运行。

叫并行。


4. 并发(Concurrency)

条件: 一个CPU

CPU:快速切换进程。

例如:

复制代码
时间:

0ms  微信

10ms 浏览器

20ms 游戏

30ms 微信

看起来同时运行。

实际上CPU不断切换。

叫并发。

四.进程切换

进程切换就是CPU从执行一个进程,切换到执行另一个进程的过程。

这个过程叫:上下文切换(Context Switch)

1.CPU与runqueue(运行队列)

什么是runqueue?

CPU不能同时运行所有进程。

所以:

每个CPU都有一个:runqueue(运行队列)

里面保存:等待CPU执行的进程。

例如:

一个CPU:

复制代码
CPU0

runqueue

进程A
进程B
进程C

CPU按照调度规则:

从runqueue中选择一个进程运行。


多CPU情况

例如:

4核CPU:

复制代码
CPU0 → runqueue0

CPU1 → runqueue1

CPU2 → runqueue2

CPU3 → runqueue3

问题:

如果:

复制代码
CPU0:
100个进程


CPU1:
2个进程

那么:

CPU0压力很大,

CPU1很空闲。

所以需要:负载均衡(load balance)

让进程在CPU之间迁移。

2 .进程优先级

Linux进程优先级分两类:


1. 普通优先级

范围:100 ~ 139

普通进程:

比如:

  • 浏览器
  • 微信
  • 编辑器

都属于这个范围。


2. 实时优先级

范围:0 ~ 99

实时进程:

例如:

  • 工业控制
  • 音频处理

特点:

优先级非常高


为什么普通优先级是100~139?

之前学习nice值:

复制代码
-20 ~ 19

一共:

40个等级。

对应:

复制代码
100 ~ 139

关系:

复制代码
nice=-20

优先级=100


nice=19

优先级=139

注意:

还是:数字越小,优先级越高。

3.活动队列(active queue)

什么是活动队列?

活动队列保存: 时间片还没有结束的进程。

例如:

进程:

复制代码
A 剩余时间片5ms

B 剩余时间片10ms

C 剩余时间片2ms

它们进入:

active队列。


active队列结构

Linux设计:

复制代码
queue[140]

表示:

140个优先级队列。

结构:

复制代码
queue[0]

queue[1]

queue[2]

...

queue[139]

每一个数组元素:

就是一个进程队列。


例如:

优先级:

复制代码
105

那么放入:

复制代码
queue[105]

相同优先级如何排队?

采用:FIFO

First In First Out

先进先出。

例如:

queue120:

复制代码
A → B → C

调度:先A再B再C

nr_active

表示:当前活动队列中有多少个运行状态进程。

例如:

复制代码
nr_active=50

表示:有50个进程等待调度。


CPU如何选择进程?

过程:

第一步遍历:

复制代码
queue[140]

从:

复制代码
0

开始。


第二步:

找到第一个非空队列。

例如:

复制代码
queue[0] 空

queue[1] 空

queue[2] 有进程

那么选择:

复制代码
queue[2]

因为数字越小:

优先级越高。


第三步:

取该队列第一个进程。

例如:

复制代码
queue[2]

A → B → C

选择:A

开始运行。


但我们通常不使用这种方法

虽然遍历140个队列:

数量固定。

时间复杂度:

复制代码
O(140)

看起来是常数。

但是Linux追求极致性能。

如果每次调度都扫描:

还是浪费。

所以使用bitmap。


bitmap优化

bitmap:位图。

140个优先级:

需要:140个标记。

例如:

复制代码
queue:

0 空
1 空
2 有
3 空
4 有

bitmap:

复制代码
0010100

表示:

哪些队列非空。


Linux使用:

复制代码
5 × 32bit

因为:

复制代码
5*32=160

大于140。

这样可以快速找到第一个非空队列。


4 .过期队列(expired queue)

除了:

active队列

还有:

expired队列。


active队列:

保存:时间片没有用完的进程。


expired队列:

保存:时间片已经耗尽的进程。

例如进程A:

时间片:

复制代码
10ms

运行10ms以后:

时间片结束。

进入:

复制代码
expired队列

为什么需要expired队列?

避免某些进程一直占用CPU。

例如三个进程:

复制代码
A
B
C

如果A一直优先:

可能:B、C饿死。

所以时间片结束:

换队列。


5 .active指针和expired指针

系统维护两个指针:

复制代码
active

expired

分别指向两个队列。

例如:

复制代码
active指针
    |
    ↓

active队列


expired指针
    |
    ↓

expired队列

调度过程

开始:

复制代码
active:

A B C


expired:

D E F

CPU运行:

A、B、C

时间片耗尽:

移动到expired。

变成:

复制代码
active:

空


expired:

D E F A B C

交换指针

当active空了。

不用搬数据

只需要交换两个指针。

之前:

复制代码
active ---> 空

expired ---> D E F

交换:

复制代码
active ---> D E F

expired ---> 空

瞬间又有新的活动队列。

效率非常高。

相关推荐
binqian2 小时前
【linux】OpenSSH升级文档
linux·运维·网络
吴声子夜歌2 小时前
Shell编程——if条件语句
linux·运维·shell
Dawn-bit3 小时前
Linux 运维基础扩展:跳板机、堡垒机与物理服务器全流程
linux·运维·服务器·云计算·运维开发
新时代牛马3 小时前
Linux 定时任务:crontab、anacron 与systemd timer
linux·运维·服务器
mounter6253 小时前
Linux 7.2 引入更灵活的块设备错误注入机制
linux·linux kernel·kernel·disk·error injection
cpolar技术支持4 小时前
Docker 容器启动失败怎么查?端口、日志、权限与网络排障完整教程
linux·docker·容器·cpolar·网络排障
布莱克6054 小时前
Linux 常用命令详解:从入门到实战
linux·运维·服务器
朽棘不雕5 小时前
可执行程序的格式
linux
IT摆渡者6 小时前
11Panel 部署雷池WAF
linux