前言
本期分享Linux超重要的知识:进程概念!
博主水平有限,不足之处望请斧正!
要学习进程,我们首先得搭建宏观的计算机和操作系统结构,从更高的视角学习。
先导
计算机体系结构
使用最多提及最多的计算机结构当属 冯·诺依曼计算机结构,来看看。
了解结构,可得出结论: CPU只和内存打交道(也会控制输入输出设备和存储器)。
*CPU虽然很快,但没那么灵光,只会被动接收和执行指令。为了能够接受指令,CPU有自己的指令集(精简/复杂指令集)。所谓编译源文件为二进制可执行程序,其实就是按照CPU指令集来编译,形成此CPU可以执行的程序。
现象解释
-
【程序运行必须要加载到内存,为什么?】
:程序存在磁盘,CPU执行程序不直接和外设沟通,加载到内存后CPU才能和程序沟通。
-
【printf运行完怎么不显示?】
:printf打印的信息由CPU输出到内存,按期刷新,并不是直接输出到输出设备如显示器。
-
【内存怎么知道输入设备输入了数据?】
: CPU虽然“只和内存打交道”,但也会控制输入输出设备和存储器。
-
【从计算机体系结构理解,聊天是啥样的?】
:键盘输入到内存 => CPU计算 => 放回内存 => 输出到网卡并传输到对方网卡 =>对方网卡接收到数据后输入到内存 => CPU计算 => 放回内存 => 输出到显示器
操作系统体系结构
从图中可以得知,
操作系统是一个进行软硬件资源管理的软件,向下通过驱动管理好硬件,向上通过系统调用提供良好的运行环境。
向下管理软件
【为什么要管理?】
通过合理管理软硬件资源(方式),给用户提供良好(稳定、安全、高效)的运行环境(目的)。
【怎么管理?】
生活中所有事无非就是 决策或执行。个体常常既是决策者也是执行者;但对于一个组织,决策和执行融合效率不高,所以都是决策执行分离。
通过学校的例子来理解:
校长不需要和学生打交道也能管理学生,辅导员收集学生信息(姓名、班级和学号等),校长再通过这些数据来做决策,从而达到管理学生的目的。
-
校长就像OS(通过数据做决策,达到管理目的)
-
辅导员就像驱动( 收集数据和执行决策)
-
学生就像硬件(被管理)
伪代码:
//抽象
struct stu
{
struct stu* next; //下一个结点
char* name;
char* sex;
int age;
//...
}
void insert(...) {...};
void erase(...) {...};
int main()
{
//具象:定义结构体变量
struct stu ZhangSan = { ... };
struct stu LiSi = { ... };
struct stu WangWu = { ... };
//一种组织方式:链表
ZhangSan->next = Lisi;
Lisi->next = Wangwu;
//操作
insert(...);
erase(...);
return 0;
}
用stu
结构体抽象学生 ==> 定义stu
结构体变量 ==> 用链表组织 ==> 操作链表。
对学生的管理 ==> 对学生信息和属性的管理 ==> 对结构体变量的管理 ==> 对链表的管理。
可得结论:
管理的本质 = 对数据做管理
管理的方法 = 抽象 + 具象 + 组织 + 操作
计算机中也是如此:
伪代码:
//抽象
struct dev_struct
{
struct stu* next; //下一个结点
char* dev_type;
char* dev_status;
//...
}
void insert(...) {...};
void erase(...) {...};
int main()
{
//具象:定义结构体变量
struct dev_struct disk1 = {XXX, XXX, XXX, ...};
struct dev_struct disk2 = {XXX, XXX, XXX, ...};
//一种组织方式:链表
disk1->next = disk2;
//操作
insert(...);
erase(...);
return 0;
}
用dev_struct
结构体抽象 ==> 定义dev_struct
结构体变量 ==> 用链表组织 ==> 操作链表。
对硬件的管理 ==> 对硬件信息和属性的管理 ==> 对结构体变量的管理 ==> 对链表的管理。
“管理”,精华与进化
这个“管理”,非常精华。怎么说?
譬如我们写三子棋,本质就是管理棋盘——抽象棋盘、具象棋盘、组织棋盘、操作棋盘;我们写通讯录,本质就是管理通讯录——抽象通讯录和人、具象通讯录和人,把人放进通讯录、组织通讯录和通讯录中的人、管理通讯录和通讯录中的人。
又或者说是高级语言的出现,如C++、Java。我们写程序上来就是写一个类,也能使用已经准备好的容器。为什么?
因为这些高级语言看到了软件的本质:完成某种管理工作。
语言中许多设计都是为了方便我们完成管理工作,比如容器、算法等等。
能明白所有软件的精华都是完成某种管理工作,从而在语言层面提供高效率的“四步走”流程,这何尝不是一种“进化”?
向上提供良好运行环境
通过银行的例子理解:
到银行办业务的时候,都是通过一大块玻璃(强度极高)的一个小口和业务员沟通
【为什么要“小口”?】
去掉玻璃不行吗?
银行里有大量的“万恶之源”,很容易受到攻击——你存个钱,自己跑进钱库放进去,不现实!
所以银行需要保护自己。
那既然是为了安全,彻底封死不是更安全吗?
银行的意义,就是提供存取款服务,满足用户的需求。
“小口”就是权衡了这两个问题的一个解:接口式服务,你给我资料,我办完业务给你个结果。
既保护了自己,也保障了需求。
【怎么提供?】
给用户提供系统调用(系统接口)——即保护操作系统,也保障需求。
- 保护操作系统:OS很脆弱,不能随意访问,一点修改就能让它崩掉。
- 保障需求:尽管OS很脆弱,但它存在的意义就是为了给我们良好运行环境,一个系统调用都不提供,就没办法保障原始的需求。
但,仅仅是打印"hello world!"到显示器,就得调用系统接口,岂不是太麻烦了?
是的,所以有:
- shell:通过指令操作调用系统接口
- lib:通过库函数调用系统接口
- 图形化界面:通过图形化界面和鼠标点击调用系统接口
宏观的结构清晰了,就可以更好地学习进程。
是什么
进程是 加载到内存的程序。
*程序本质:磁盘上的文件。
进程多起来以后,OS要不要把它们管理起来?当然要的,怎么管理呢?
【当内存中进程很多,如何管理?】
同样的四步走,但是,进程要怎么抽象?
#进程的抽象
宏观来讲,
A程序载入内存变为A进程,A进程的所有属性和信息(包括A程序的代码和数据)都被放在一个叫做**PCB(进程控制块)**的结构体变量中,也意味着,PCB是进程的抽象。
PCB是操作系统中宏观的概念,到具体的操作系统,
Linux中, task_struct
是进程的抽象。
所以,进程也可以说是 内核数据结构task_struct
+ 某程序在磁盘内的代码 + 数据。
前者的各种属性允许操作系统进行管理,调度等操作,后者是CPU执行需要用到的。
那task_struct
里有些什么呢?
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
int lock_depth; /* BKL lock depth */
/* ... */
}
所以Linux中,这样管理进程:
用task_struct
结构体抽象 ==> 定义task_struct
结构体变量 ==> 内核数据结构组织 ==> 内核算法操作。
即
对进程的管理 ==> 对进程信息和属性的管理 ==> 对结构体变量的管理 ==> 对链表的管理。
知道如何抽象进程就可以得出,当sort.exe
加载进内存,操作系统帮我们做了这些事来管理:
//具象:创建一个进程控制块
struct task_struct sort = malloc(sizeof(struct task_struct));
sort->status = XXX; //填入属性
sort->priority = XXX; //填入属性
//组织
sort->next = XXX; //一种组织方式:链表
//操作
insert(...);
erase(...);
//...
怎么操作
查看进程
ps ajx
:查看所有进程
ps ajx | head -1
:显示第一行
[bacon@VM-12-5-centos linux]$ ps ajx | head -1
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
*pid是某进程的编号,ppid是某进程的父进程的编号,STAT是进程状态,等会会讲到。
ps ajx | grep [process]
:查找进程
*grep也是个进程,查找process,所以grep “process” 也能看到grep
cd /proc/
:进入以pid命名的进程目录,查看
可知进程特性:是文件
进入对应目录还能看到:
“exe”链接至磁盘某个位置的可执行程序。
如果我们把磁盘上的可执行程序删掉,会怎么样?
正在运行的process还能运行,因为它已经加载到内存。
进程相关的系统调用
getpid(获取进程id)
NAME
getpid - get the process ID
SYNOPSIS
#include <unistd.h>
pid_t getpid(void);
DESCRIPTION
The getpid() function shall return the process ID of the calling process.
RETURN VALUE
The getpid() function shall always be successful and no return value is reserved to indicate an error.
getppid(获取父进程id)
NAME
getppid - get the parent process ID
SYNOPSIS
#include <unistd.h>
pid_t getppid(void);
DESCRIPTION
The getppid() function shall return the parent process ID of the calling process.
RETURN VALUE
The getppid() function shall always be successful and no return value is reserved to indicate an error.
看一个现象:
process每次运行的pid都不同,能理解,因为每次都是重新加载进内存,但是ppid为什么都一样?
:大部分进程都是作为命令行解释器bash的子进程运行。
为什么呢?
结合一个例子理解:
p村的张三,是村长的儿子,对隔壁村的如花很中意,叫王婆这个媒婆牵牵线。王婆去了,但是如花已经有心上人了,王婆转告张三。但张三不死心,三番两次让王婆去,王婆不愿意还他摆出官架子。不想得罪村长,也不想无意义地跑来跑去,就想出一个办法:开一家“世只因佳缘”婚姻介绍所,招一堆实习生,张三再让她去她就派实习生去——事情没办好就是实习生的问题,不管我王婆的事!
以子进程的方式运行,不管你子进程干了什么,都不影响我bash,与我无关!
int main()
{
printf("Process %d running..., ppid %d\n", getpid(), getppid());
int* p = NULL;
*p = 10;
return 0;
}
就算子进程发生Segmentation fault段错误,也不影响我bash——bash依然正常执行指令。
kill -9(杀死进程)
可以运行,可以被杀掉,可知进程特性:具有动态属性
*kill -l
查看kill指令的参数
fork(创建子进程)
NAME
fork - create a new process
SYNOPSIS
#include <unistd.h>
pid_t fork(void);
DESCRIPTION
The fork() function shall create a new process. The new process (child
process) shall be an exact copy of the calling process
//子进程和父进程只有少部分内容和属性不同
int main()
{
//只有父进程
fork();
//有父进程 + 子进程
printf("process %d running, ppid %d\n", getpid(), getppid());
return 0;
}
[bacon@VM-12-5-centos 10]$ ./process
process 24840 running, ppid 19523 #父进程
process 24841 running, ppid 24840 #子进程
创建子进程后,执行流分流,对于后续代码,父子进程各自都要执行。
若我想父进程和子进程执行自己的代码,而不是执行同样的代码呢?
利用返回值。
- 创建成功的返回值:
- 给父进程返回子进程pid
- 给子进程返回0
- 创建失败的返回值:
- 给父进程返回-1
- 子进程不会被创建
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
}
else if(id > 0)
{
printf("process %d running, ppid %d, id %d\n", getpid(), getppid(), id);
}
else
{
printf("process %d running, ppid %d, id %d\n", getpid(), getppid(), id);
}
return 0;
}
[bacon@VM-12-5-centos 10]$ ./process
process 25997 running, ppid 19523, id 25998
process 25998 running, ppid 25997, id 0
效果是达到了,但是看着怎么不对劲?
同一个变量id有两个不同的值?
不同分支同时执行?
这两 个问题先按下不表。
特性
- 进程是文件
- 进程具有动态属性
- 大部分进程作为bash的子进程运行
#进程状态
是什么
是task_struct
中的一个属性(整数),用来描述进程当前状态。
/*
* 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 */
};
*上为Linux描述进程状态的结构体
为什么
不同的运行场景,需要不同的进程状态表示。
有哪些
读者朋友们可能看到过这些状态:运行、新建、就绪、挂起、阻塞、等待、停止、死亡等等。
接下来谈谈,
先理解宏观的进程状态,再落到具体的操作系统层面“降维理解”Linux的进程状态。
宏观的进程状态
进程状态用语言描述没法真正理解,我们同样地来一张图,了解三种宏观的进程状态:运行、阻塞和挂起。
蓝色的走线,代表程序从磁盘加载到内存,变成进程,最后运行的过程。即将运行的进程,都先入run_queue,而后由CPU调度运行。为什么有runqueue?CPU很快,如果仅将正在执行的进程认作“运行”状态,那这个状态转瞬即逝,作用就没那么大。
绿色的走线,代表进程访问外设的过程。当需要访问外设(如磁盘),找到对应的结构体对象(disk1),调用其读写方法进行访问。CPU的速度很快,进程可以很快轮转;但外设很慢,进程多了就只能排队。而且CPU不可能陪着外设等,所以外设结构体对象都有自己的wait_queue,CPU会把需要访问外设的进程都扔到其wait_queue,然后接着回去执行其他进程。但wait_queue总也是有限的,当等待着要访问的进程都放不下,就需要只保留基本结构(仍被组织着),将数据和代码换出到磁盘,等有位置了再从磁盘换入。
其中,
- 在CPU的运行队列 run_queue 中的进程都是 运行状态
- 在外设结构体对象的等待队列 wait_queue 中的进程都是 阻塞状态
- 因wait_queue满,无法入队,数据和代码被换出到磁盘的进程都是 挂起状态
进程状态的不同,其实就是进程所处的队列不同。
以上是宏观操作系统理论中的进程状态,具体操作系统的进程状态都以它们为基础。
接下来看看Linux的进程状态。
具体的进程状态(Linux)
-
R(runing) 运行:入了run_queue的进程
-
S(sleeping) 休眠:阻塞的一种
-
T(stop) 停止:可以用kill让它停止
-
t(tracing stop) 停止(被追踪):如调试
-
D(disk sleep) 深度睡眠:D状态相当于免死金牌,几乎无法被终止
-
Z(zombie) 僵尸:进程运行完,等待父进程获取结束信息的状态
-
X(dead) 死亡:进程彻底完事(一般观察不到,这个状态可能都不是设置给用户看的)
-
后缀"+":表示前台运行(若没有,代表进程后台运行)
【为什么要有Z状态?】
有时候,父进程需要知道子进程任务完成得如何,需要知道子进程退出信息,所以子进程死亡不能立即全部回收资源,得等父进程来获取结束信息。
接下来实际看看这些状态
*会用到一个监控脚本,直接在bash运行
while :; do ps ajx | head -1 && ps ajx | grep myproc; sleep 1; done
R
让进程死循环运行就可以观察到R+前台运行状态。
S
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("running...\n");
sleep(1);
}
return 0;
}
T
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1)
{
printf("%d running...\n", getpid());
sleep(1);
}
return 0;
}
kill -l
查看kill所有信号。
t
D
目前没法观察。
Z
defunct是“死掉的”的意思,标识这个进程已经死了,成为僵尸进程。不回收则会造成内存泄漏。
X
目前没法观察。
#孤儿进程
如果父进程先于子进程退出,子进程就变成孤儿进程,会被操作系统1号进程领养,最终由1号init进程回收。
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
while(1)
{
printf("child process, pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
else
{
int cnt = 5;
while(cnt)
{
printf("parent process, pid:%d, ppid:%d\n", getpid(), getppid());
--cnt;
sleep(1);
}
printf("parent process exit!\n");
}
return 0;
}
#进程优先级(priority)
是什么
决定进程运行先后顺序的属性。也就是task_struct中的一个整数。
优先级用 “nice值” 做区分,优先级 = 默认优先级 + nice值。(优先级的值越小,优先级越高,越先执行)
为什么
为什么有优先级?
资源有限,得排队,重要的可以往前排。
怎么操作
怎么查看
ps -elf
:查看当前进程的状态
- UID:执行者身份
- PRI:优先级,priority的缩写
- NI:nice值,nice的缩写
怎么修改
通过nice值来修改
nice∈[-20, 19], 默认80(Linux用两位整数来表示优先级,nice值若不限制,可能影响调度器的均衡性)
[bacon@VM-12-5-centos 5]$ sudo top #修改优先级需要root权限
#输入r:renice
PID to renice [default pid = 1] 26848
Renice PID 26848 to value -20
#修改完成...
[bacon@VM-12-5-centos 5]$ ps -elf | head -1 && ps -elf | grep process
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
0 S bacon 26848 11501 0 60 -20 - 1054 hrtime 22:47 pts/1 00:00:00 ./process
sudo top
==> r
==> pid
==> nice value
#进程相关概念
- 竞争性:进程总是比CPU多的,为了高效完成任务,更合理竞争相关资源,就有了优先级
- 独立性:进程之间互相独立,互不影响
- 并行:多个进程在多个CPU下分别同时运行
- 并发:多个进程在单个CPU下运行(采用进程切换的方式)
独立性的验证:
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
int* p = NULL;
*p = 100;
}
else
{
while(1)
{
printf("parent process, pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
[bacon@VM-12-5-centos 11]$ ./process
parent process, pid:7701, ppid:25597
parent process, pid:7701, ppid:25597
parent process, pid:7701, ppid:25597
parent process, pid:7701, ppid:25597
^C
子进程解引用野指针,发生段错误,不影响父进程
进程切换:CPU给不同进程分配时间片(A进程运行一会,B进程运行一会,不断轮转),就实现了不同进程在一个CPU上同时推进。
如何轮转的?
我们知道进程运行时会占用CPU,但不是一直占有的。比如写了个死循环给CPU,它也能执行别的进程。既然如此,一定会存在这样的情况:某个进程没跑完就被拿下去了。那下次回来的时候……
通过大学当兵的例子来理解:
大学时,可以保留学籍去当兵。 但张三没给学校打好招呼,而是直接跑去部队。当完兵回学校发现被勒令退学了。学校的视角,张三这个娃课也不上,试也不考,退学!
进程们“离校”,进程切换,得和CPU打好招呼,保护上下文信息——上下文保护。
进程们“返校”,恢复运行,得和CPU打好招呼,恢复上下文信息——上下文恢复。
本期的分享就到这里,感谢浏览
这里是培根的blog,期待和你共同进步
下期见~