进程概念
- 进程是正在运行的程序的实例,它由程序、数据和进程控制块三部分组成。
- 进程信息被放在进程控制块中,简称PCB(process control block)
- Linux操作系统下的PCB是: task_struct ,它是一种结构体。
- 所有运行在系统里的进程都以 task_struct 链表的形式存在内存中,程序的代码和数据也会被加载到内存中。
其实计算机管理硬件的一个方法就是先描述,再组织。用struct描述被管理的对象,用特定的数据结构再组织起来。
task_struct 内容分类
- 标识符:描述本进程的唯一标识符,用来区别其他进程。
- 状态:任务状态,退出代码,退出信号等。
- 优先级:相对于其他进程的优先级。
- 程序计数器:程序中即将被执行的下一条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块指针。
- 上下文数据:进程执行时,CPU 寄存器中的数据
- I / O 状态信息:包括显示的 I / O 请求,分配给进程的 I / O 设备和被进程使用的文件列表。
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
查看进程
一、
显示所有进程
ps ajx
我们也可以先写一个 c 程序
#include <stdio.h>
#include <unistd.h>
int main()
{
while (1)
{
printf("I am a process\n");
sleep(1);
}
return 0;
}
运行起来之后,右击选项卡,选择复制SSH渠道
在新的终端中使用 ps ajx | grep 'mytest'
可以找到自己的这个进程
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ps ajx | grep 'mytest'
2888 25600 25600 2888 pts/0 25600 S+ 1001 0:00 ./mytest
25432 28960 28959 25432 pts/1 28959 S+ 1001 0:00 grep --color=auto mytest
但是这里显示了两行,第一行当然是我们自己写的程序,第二行的是 grep
命令执行的进程。
每个进程在系统中,都会存在一个唯一的标识符,称为 pid
通过下面的指令可以查看pid:
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ps ajx | head -1 && ps ajx | grep 'mytest' | grep -v grep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
2888 25600 25600 2888 pts/0 25600 S+ 1001 0:00 ./mytest
head -1
显示第一行(表头),&&
相当于逻辑与,前面的指令执行成功就执行后面的指令,后面就是找到 mytest
进程,去掉 grep
命令执行的进程。
二、
在 /proc
下,实时存在着当前的进程信息:
通过 pid
可以找到某个进程,比如 25600
是我们 mytest
程序的进程:
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ls /proc/25600
attr coredump_filter gid_map mountinfo oom_score sched statm
autogroup cpuset io mounts oom_score_adj schedstat status
auxv cwd limits mountstats pagemap sessionid syscall
cgroup environ loginuid net patch_state setgroups task
clear_refs exe map_files ns personality smaps timers
cmdline fd maps numa_maps projid_map stack uid_map
comm fdinfo mem oom_adj root stat wchan
在这之中可以看到 cwd
就是该进程当前的工作路径,exe
就是进程对应的可执行程序的磁盘文件:
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ls -al /proc/25600
total 0
//...
lrwxrwxrwx 1 CegghnnoR CegghnnoR 0 Aug 12 17:35 cwd -> /home/CegghnnoR/code/2022_8_12
-r-------- 1 CegghnnoR CegghnnoR 0 Aug 12 19:58 environ
lrwxrwxrwx 1 CegghnnoR CegghnnoR 0 Aug 12 17:35 exe -> /home/CegghnnoR/code/2022_8_12/mytest
//...
通过系统调用获取标识符pid/ppid
系统调用和库函数的概念
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
getpid
获取当前进程的 pid,头文件 <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while (1)
{
printf("I am a process, pid:%d\n", getpid());
sleep(1);
}
return 0;
}
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ./mytest
I am a process, pid:3997
I am a process, pid:3997
//...
要杀掉该进程,除了使用 ctrl+c
,也可以使用 kill -9 [pid]
使用 getppid
可以获取父进程id(ppid)
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ./mytest
I am a process, pid:6008, ppid:4583
^C
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ./mytest
I am a process, pid:6023, ppid:4583
^C
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ./mytest
I am a process, pid:6040, ppid:4583
^C
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ./mytest
I am a process, pid:6042, ppid:4583
^C
每次启动进程,pid都会发生变化,ppid不变。
因为几乎我们在命令行上所执行的所有的指令,都是 bash 进程的子进程。
代码创建子进程 fork()
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
printf("hello world id:%d\n", id);
return 0;
}
结果:
hello world id:6243
hello world id:0
printf执行了两次,id为0的是子进程,>0的是父进程。
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
while (1)
{
printf("我是子进程,pid:%d,我的父进程pid:%d\n", getpid(), getppid());
sleep(1);
}
}
else
{
while (1)
{
printf("我是父进程,pid:%d,我的父进程pid:%d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ./mytest
我是父进程,pid:9668,我的父进程pid:2170
我是子进程,pid:9669,我的父进程pid:9668
- fork 之后,父进程和子进程会共享代码,一般都会执行后续的代码。
- fork 之后,父进程和子进程返回值不同,给父进程的返回值是子进程的pid,子进程的返回值是0可以通过判断不同的返回值让父子进程执行不同的代码块。
进程状态
概念
运行状态:进程只要在运行队列中就处于运行状态,代表已经准备好随时被调度。
终止状态:进程还在,只不过永远不运行了,随时等待被释放。
因为释放也占用资源,所以终止状态存在的意义就是,在操作系统较忙时可以先安排其他更重要的工作,稍后对终止状态的进程进行释放
阻塞状态:
-
进程不仅需要向 CPU 申请资源,也可能会向磁盘、网卡、显卡等申请资源。如果向CPU申请资源暂时无法满足,则会在运行队列中排队。如果向其他设备申请资源,也是需要排队的。
-
当进程需要访问某些资源(磁盘网卡等)时,该资源如果暂时没有准备好,或者正在给其他进程提供服务,此时要将当前进程从运行队列中移除放到相应设备的等待队列。这样 CPU 就可以继续运行后面的进程,提高效率。(底层其实就是 PCB 的移动)
在这个等待过程中,该进程的代码不会被执行,看上去就和卡住了一样,这被称为进程阻塞。
挂起状态:
如果内存不足,而有一些进程短时间内不会被调度(等待的资源短时间内不会就绪),它的代码和数据还存在内存中就是一种浪费,操作系统就会把该进程的代码和数据置换到磁盘上,只留 PCB 在内存中,这样的进程就处于挂起状态。
Linux 进程状态
以上是操作系统原理中的概念,具体到 Linux 系统中稍微有一点不一样。
Linux内核源代码里的定义:
/*
* 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 */
};
写一个不停打印 Hello world 的程序:
#include <stdio.h>
int main()
{
while (1)
{
printf("Hello world\n");
}
return 0;
}
运行后查看该进程的状态:
[CegghnnoR@VM-4-13-centos file]$ ps ajx | head -1 && ps ajx | grep 'mytest' | grep -v 'grep'
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
2170 26046 26046 2170 pts/0 26046 S+ 1001 0:08 ./mytest
STAT一栏显示的就是该进程的状态,为S+,S代表睡眠的意思,其实就是阻塞状态,但是这边明明正在不停地打印Hello world,为什么不是运行状态呢?
其实该程序要运行的代码就一行printf,CPU的速度是极快的,但是显示器的速度慢,绝大部分时间进程都在等待显示器的资源。
如果你把printf() 一行去掉,再运行看程序状态:
#include <stdio.h>
int main()
{
while (1)
{
}
return 0;
}
[CegghnnoR@VM-4-13-centos file]$ ps ajx | head -1 && ps ajx | grep 'mytest' | grep -v 'grep'
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
2170 29435 29435 2170 pts/0 29435 R+ 1001 0:11 ./mytest
这个进程不用访问其他外设,只等 CPU 的资源,而且是死循环,状态显示R+,也就是运行状态。
S状态也叫浅度睡眠,或可中断睡眠,意为可以随时被唤醒,或者手动去终止它。
D状态也是一种阻塞状态,一般指等待磁盘资源。该状态也叫深度睡眠,或不可中断睡眠,因为磁盘的写入删除都要返回一个成功或失败的反馈信息,在这期间该进程必须等待,如果被杀,会导致错误,所以D状态是对该进程的一种保护,使它不可被杀掉。
X状态是死亡状态,也就是上面说的终止状态。
Z状态是僵尸状态,当一个Linux中的进程退出的时候,一般不会直接进入X状态,而是进入Z状态,这是为了维护进程的 PCB 中的退出信息,从而让父进程或者操作系统读取。
僵尸进程的模拟及其危害
用fork创建一个子进程,然后子进程正常运行5s后退出,父进程一直死循环。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt)
{
printf("我是子进程,我还剩%ds\n", cnt--);
sleep(1);
}
printf("我是子进程,我已经变僵尸了,等待被检测\n");
exit(0);
}
else
{
while (1)
{
sleep(1);
}
}
return 0;
}
结果:
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ./mytest
我是子进程,我还剩5s
我是子进程,我还剩4s
我是子进程,我还剩3s
我是子进程,我还剩2s
我是子进程,我还剩1s
我是子进程,我已经变僵尸了,等待被检测
^C
同时在另一个终端中使用指令每隔一秒查看这对父子进程的状态:
[CegghnnoR@VM-4-13-centos 2022_8_12]$ while :; do ps ajx | head -1 && ps ajx | grep 'mytest' | grep -v 'grep'; sleep 1; echo "###################################"; done
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
26059 29858 29858 26059 pts/1 29858 S+ 1001 0:00 ./mytest
29858 29862 29858 26059 pts/1 29858 S+ 1001 0:00 ./mytest
###################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
26059 29858 29858 26059 pts/1 29858 S+ 1001 0:00 ./mytest
29858 29862 29858 26059 pts/1 29858 S+ 1001 0:00 ./mytest
###################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
26059 29858 29858 26059 pts/1 29858 S+ 1001 0:00 ./mytest
29858 29862 29858 26059 pts/1 29858 S+ 1001 0:00 ./mytest
###################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
26059 29858 29858 26059 pts/1 29858 S+ 1001 0:00 ./mytest
29858 29862 29858 26059 pts/1 29858 S+ 1001 0:00 ./mytest
###################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
26059 29858 29858 26059 pts/1 29858 S+ 1001 0:00 ./mytest
29858 29862 29858 26059 pts/1 29858 S+ 1001 0:00 ./mytest
###################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
26059 29858 29858 26059 pts/1 29858 S+ 1001 0:00 ./mytest
29858 29862 29858 26059 pts/1 29858 Z+ 1001 0:00 [mytest] <defunct> #变成僵尸状态
###################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
26059 29858 29858 26059 pts/1 29858 S+ 1001 0:00 ./mytest
29858 29862 29858 26059 pts/1 29858 Z+ 1001 0:00 [mytest] <defunct>
###################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
26059 29858 29858 26059 pts/1 29858 S+ 1001 0:00 ./mytest
29858 29862 29858 26059 pts/1 29858 Z+ 1001 0:00 [mytest] <defunct>
###################################
^C
可以看到,5s后子进程变成了僵尸进程。
如果父进程一直不回收僵尸状态的子进程,那么该状态就一直维护着,该进程的相关资源不会被释放,造成内存泄漏!
孤儿进程
顾名思义,就是父进程提前退出,子进程还在运行。
父进程退出有bash进程回收,然后子进程会被1号进程(操作系统)领养。子进程退出会被操作系统回收。
模拟孤儿进程:
父进程运行三秒后退出,子进程死循环
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
while (1)
{
printf("我是子进程\n");
sleep(1);
}
}
else
{
int cnt = 3;
while (cnt)
{
printf("我是父进程,我:%d\n", cnt--);
sleep(1);
}
exit(0);
}
return 0;
}
[CegghnnoR@VM-4-13-centos 2022_8_12]$ while :; do ps ajx | head -1 && ps ajx | grep 'mytest' | grep -v 'grep'; sleep 1; echo "###################################"; done
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
26059 3847 3847 26059 pts/1 3847 S+ 1001 0:00 ./mytest
3847 3848 3847 26059 pts/1 3847 S+ 1001 0:00 ./mytest
###################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
26059 3847 3847 26059 pts/1 3847 S+ 1001 0:00 ./mytest
3847 3848 3847 26059 pts/1 3847 S+ 1001 0:00 ./mytest
###################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
26059 3847 3847 26059 pts/1 3847 S+ 1001 0:00 ./mytest
3847 3848 3847 26059 pts/1 3847 S+ 1001 0:00 ./mytest
###################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 3848 3847 26059 pts/1 26059 S 1001 0:00 ./mytest
###################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 3848 3847 26059 pts/1 26059 S 1001 0:00 ./mytest
👆🏻:3 秒后父进程消失,子进程的 ppid
变成 1,表示被操作系统领养了。
另外,子进程的状态由 S+
变为 S
,有 +
号的表示前台进程,无 +
号的表示后台进程,前台进程可以使用 ctrl+c
终止,后台进程只能用 kill -9 [pid]
的方式终止。
暂停状态
T状态和t状态都表示暂停。
T状态表示常规的暂停,比如 kill -19 [pid]
暂停的进程:
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ps ajx | head -1 && ps ajx | grep mytest | grep -v grep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
26059 7716 7716 26059 pts/1 7716 S+ 1001 0:04 ./mytest
[CegghnnoR@VM-4-13-centos 2022_8_12]$ kill -19 7716 #输入暂停命令
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ps ajx | head -1 && ps ajx | grep mytest | grep -v grep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
26059 7716 7716 26059 pts/1 26059 T 1001 0:07 ./mytest #变为T状态
另:使用 kill -18 [pid]
可以让进程继续执行。使用 kill -l
可以查看选项。
t状态表示进程被调试的时候,遇到断点时所处的状态。
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ps ajx | head -1 && ps ajx | grep mytest | grep -v grep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
26059 9465 9465 26059 pts/1 9465 S+ 1001 0:00 gdb mytest
9465 9602 9602 26059 pts/1 9465 t 1001 0:00 /home/CegghnnoR/code/2022_8_12/mytest
总结
- 运行状态对应R状态
- 终止状态对应Z状态和X状态
- 阻塞状态对应S状态或D状态
- 挂起状态对应S状态或D状态或T/t状态
这就是操作系统原理和 Linux 具体实现的区别。
进程优先级
进程优先级就是各个进程获取某种资源的先后顺序。
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ps -al
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1001 14456 26059 6 80 0 - 1833 n_tty_ pts/1 00:00:00 mytest
0 R 1001 14474 2170 0 80 0 - 38595 - pts/0 00:00:00 ps
Linux 下进程优先级由两部分组成:PRI 和 NI,数字越小,代表优先级越高,数字越大,代表优先级越低。
默认优先级是80,要更改进程优先级就要更改 NI,而不是更改 PRI。NI 表示进程优先级的修正数据。
NI的取值范围是-20~19,一共40个级别,PRI = 默认优先级 + NI
用 top 命令可以更改已存在进程的 NI 值
- top
- 进入 top 后按 r --> 输入进程 pid --> 输入 NI 值
其他概念
竞争性:系统进程数目众多,而 CPU 资源只有少量,所以进程之间具有竞争性,为了高效完成任务,更合理竞争资源,便有了优先级。
独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰(不会因为一个进程挂掉或者异常,而导致其他进程出现问题)。
并行:多个进程在多个 CPU 下同时运行。
并发:多个进程在一个 CPU 下采用进程切换的方式,在一段时间之内,让多个进程都得以推进。
一个 CPU 一次只能运行一个进程,其他进程都在运行队列里等待,由于并发的存在,才得以让我们在宏观上看像是多个进程在同时运行。
进程抢占:对于正在 CPU 上运行的进程,如果此时来了个更高优先级的进程,调度器会直接把低优先级进程从 CPU 上剥离,放上更高优先级的进程。
我们知道队列是不允许插队的,在这种情况下又想实现优先级该怎么办呢?
实际上一个CPU有多个运行队列,把相同优先级的进程放在同一个队列中,CPU会先选择优先级高的队列里的进程开始运行。
进程在运行中产生的各种寄存器数据,叫做上下文数据,当进程被剥离时需要保存上下文数据,当进程恢复的时候,需要将曾经保存的上下文数据恢复到寄存器中。上下文数据保存在 PCB 中。