操作系统平台:Linux
服务器系统: CentOS 7


概念抽象

程序

程序 = 代码 + 数据

程序是储存在硬盘上的可执行文件

进程

程序加载到内存后,就在内存中程序的就是进程。也就是说一个正在运行的程序就能叫做进程

结构关系如下

如图,操作系统为了管理内存中的进程,使用了PCB结构体来描述进程,通过管理PCB来管理进程,依然是先描述再组织

PCB:进程控制块的数据结构(process control block)

所以实际上:进程=PCB+代码和数据

对于代码和数据没什么好说的,接下来主要讨论PCB

task_struct

Linux平台下的PCB叫做task_struct

task_struct内容分类:

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

加粗部分会详细介绍

查看进程

进程的信息可以通过/proc 系统文件夹查看,其中文件夹的名字就是进程的PID

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

以我自己编写的一个程序为例

Makefile

1
2
3
4
5
6
mycmd:mycmd.c
gcc -o $@ $^

.PHONY:clean
clean:
rm -rf mycmd

: 后面的程序都是这三个头文件,仅修改main()函数体即可
mycmd.c

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>


int main()
{
while(1) sleep(1);//死循环使该进程常驻内存
return 0;
}

然后编译并运行程序
命令行

1
2
3
make
./mycmd
ps aux | grep mycmd | grep -v grep

进程标示符(PID PPID)

可以通过系统接口获取进程标示符

  • 进程id (pid) 使用getpid()
  • 父进程id (ppid) 使用getppid()

来修改一下代码
mycmd.c

1
2
3
4
5
6
7

int main()
{
printf("pid: %d\n",getpid());//打印pid (该进程id)
printf("ppid: %d\n",getppid());//打印ppid (父进程id)
return 0;
}

命令行

1
2
3
make clean
make
./mycmd

利用fork()创建子进程

mycmd.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

int main()
{
pid_t id = fork();
if(id == 0)//子进程
{
printf("child pid: %d\n",getpid());//打印pid (该进程id)
printf("child ppid: %d\n",getppid());//打印ppid (父进程id)
sleep(1);
}
else//父进程
{
printf("father pid: %d\n",getpid());//打印pid (该进程id)
printf("father ppid: %d\n",getppid());//打印ppid (父进程id)
sleep(1);
}
return 0;
}

如图,fork()创建了子进程,且子进程的PPID和父进程PID相同

fork()的返回值

父子进程中fork()函数的返回值(此处用变量id储存)是不同的:

父进程id的值为子进程的PID,其值>0;子进程id值固定为0

  • id > 0 父进程
  • id == 0 子进程
  • id < 0 fork()失败

父子进程分流

利用fork()返回值不同的特性可以做到分流操作,利用if...else...让父子进程执行不同的代码

戳我去fork的详细介绍

进程状态

状态在kernel源代码里定义

1
2
3
4
5
6
7
8
9
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)

R状态并不一定正在运行,而是正在运行处于运行队列中的一种

1
2
3
4
5
int main()
{
while(1);
return 0;
}

S 睡眠状态(sleeping)

S 意味着进程在等待运行完成

(这里的睡眠有时也可叫做可中断睡眠 interruptible sleep)

下面展示两种S状态的进程

1
2
3
4
5
int main()
{
sleep(50);
return 0;
}

直接使用sleep()系列的函数直接使进程休眠

1
2
3
4
5
6
7
int main()
{
int n;
printf("Enter the num: ");
scanf("%d",&n);
return 0;
}

scanf()这种需要等待外设(键盘)的接口,在阻塞等待资源的过程中会使进程进入S状态

D 磁盘休眠状态(Disk sleep)

有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。

T 停止状态(stopped)

可以通过(kill等命令)发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。

1
2
3
4
5
int main()
{
while(1) sleep(1);
return 0;
}

运行前先复制ssh渠道,其中一个窗口用于执行进程

1
2
3
make clean
make
./mycmd

另一个进程用于输入命令

先查看该进程的PID

1
ps ajx | head -1 && ps ajx | grep mycmd | grep -v grep

如图,这次的PID20275

然后用kill发送SIGSTOP,对应参数为-19

1
2
kill -19 20275
ps ajx | head -1 && ps ajx | grep mycmd | grep -v grep

可以看到它已经由S状态改为T状态了

接下来发送SIGCONT,对应参数-18,使进程恢复

1
2
kill -18 20275
ps ajx | head -1 && ps ajx | grep mycmd | grep -v grep

可以看到已经由T变为原本的S状态了

X 死亡状态(dead)

这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

Z 僵尸进程(zombie)

这个详细讨论下

产生

当该进程退出后,父进程尚未使用wait()之类的接口获取该进程的退出码,且父进程没有结束时,该进程会变成僵尸进程

父进程比子进程先退出时,子进程的父进程会改变为PID为1的进程,由新进程托管

下面创建一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main()
{
pid_t id = fork();
if(id == 0)//子进程
{
printf("child exit\n");
}
else//父进程
{
sleep(30);
printf("father exit\n");
}
return 0;
}

运行程序后的30秒内查看进程状态,可以看到子进程进入了Z状态

行为

僵尸进程会以终止状态保持在进程表中,等待父进程读取退出状态代码

危害

  • 父进程一直未获取子进程的退出码,僵尸状态就会一直保持
  • 保持Z状态的进程的PCB仍然要一直维护,占用资源
  • 未退出Z状态的子进程可能造成内存泄漏

孤儿进程

当父进程比子进程退出后,这个子进程便成了孤儿进程

既然原本的父进程没了,谁来托管子进程呢?答案是PID1的那个进程

例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main()
{
pid_t id = fork();
if(id == 0)//子进程
{
sleep(30);
printf("child exit\n");
}
else//父进程
{
sleep(1);
printf("father exit\n");
}
return 0;
}

进程优先级

基础概念

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

查看优先级

首先写一个常驻进程

1
2
3
4
5
int main()
{
while(1)sleep(1);
return 0;
}

然后使用ps -la查看进程信息

1
2
./mycmd
ps -la

其中的PRINI与进程优先级有关,PRI就是进程的优先级,跟排队摇号一样,此值越小,被执行的优先级越高,而NI就是nice值,用于修正原PRI

PRI值的计算

首先在看到的PRI值之外,还有个隐藏的基准值,本文用PRI0指代,这个PRI0是固定的,当NI值为0时,PRI == PRI0,而
无论怎么修改多少次NI,PRI的值减去NI值都相等,所以大可以推断在本系统(Linux)中,PRI值有如下计算公式

PRI = PRI0 + NI

修改NI值

因为修改NI值要管理员权限,所以要么root用户用top,要么信任用户用sudo top打开界面,然后按r,输入待修改进程的PID,按下回车后再输入新的NI值(有效范围-20~19)

此处可以用ps -la查看进程的PID,或调用getpid()

再写一个例子

1
2
3
4
5
6
7
8
9
int main()
{
while(1)
{
printf("mypid: %d\n",getpid());
sleep(1);
}
return 0;
}

sudo top然后按r

ps -la可以看到被修改后的进程优先级

其它概念

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

下一章环境变量