从程序说起,我们写好的程序在经过编译链接最后生成的可执行文件是在磁盘上放着的。
当我们运行它的时候,程序加载到内存,就成了进程,此时具有动态属性。
操作系统对内存里的数据进行管理,如果有很多个程序加载到内存,这些程序作为写到内存里的软件,操作系统就需要管理这些软件,那么操作系统该如何管理呢?
先描述,再组织。
为了管理这么多的程序,操作系统将其描述,就有了进程控制块(PCB),说白了就是定义一个结构体。
操作系统为每个程序生成对应的进程控制块,里面包括了程序的各种属性,对应的代码和属性地址,以及可能有下一个进程控制块的指针。
struct task_struct
{
//该进程的属性
//比如:进程ID、进程状态、优先级等
//该进程对应的代码地址和属性地址
struct task_struct* next;
}
这些在原来的可执行文件中是没有的,可执行文件中只有代码和数据
当程序中的代码和数据加载到内存后,操作系统会针对每一个程序匹配一个进程控制块,说白了就是一个结构体变量,这个结构体变量与其加载到内存的代码和数据联系起来。
struct task_struct* p1 = malloc(struct task_struct);
//并且加载好对应的属性信息
p1->.. = xx
p1->addr = 代码的地址
为了方便,结构体变量可以通过next链接起来,当CPU想要调度一个进程的时候,操作系统就可以直接通过PCB对进程进行对应的操作。
所谓的对进程管理,变成了对进程对应的PCB进行管理,转换成对链表的增删查改。(比如一个进程死亡了,操作系统就可以通过查找到对应的PCB,再将对应加载到内存的可执行文件删除。)
总结:
进程 = 内核数据结构(task_struct)+ 进程对应的磁盘代码。
当我们写好的程序,在编译后形成的可执行文件没有运行时并不是一个进程!
process未运行时,本身不能作为一个进程。
当程序运行,程序加载到内存中,这时才能称为进程,那么我们在Linux如何查看进程呢?
ps ajx 查看当前所有进程
ps ajx | grep process 通过配合grep获取对应进程,比如这里的process
配合 ps ajx | head -1 可以显示相关的标题
比如: 我们要查看process进程相关信息
ps ajx | head -1 && ps ajx | grep process | grep -v grep 获取process进程信息,显示标题,忽略grep相关进程信息
在上述标题中:
通过 kill -l 查看kill 相关的所有信号
通过 kill -9 进程ID 通过9号信号和相应进程PID,可以杀死对应进程
当进程正在运行时
通过 kill -19 进程ID 可以暂停进程
通过 kill -18 进程ID 可以继续进程
系统调用是操作系统为用户提供的接口,我们可以在程序中试试这些接口。
getpid() 返回子进程PID
getppid() 返回父进程PID
通过持续运行一个简单的程序,让我们在Linux中找到它。
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 while(1)
7 {
8 printf("我是一个进程,对应PID: %d\n", getpid());
9
10 sleep(3);
11 }
12 return 0;
13 }
程序运行后,通过访问linux的/proc目录,我们看到目录下有一个和进程PID匹配的文件,其实进程本身就是一个文件,该文件保存着进程的各种属性。
我们通过访问16005文件,可以看到其中exe就记录了操作系统在哪加载的路径。
一旦程序运行结束,或者杀死进程,这些也就消失了。
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 while(1)
7 {
8 printf("我是一个进程,对应PID: %d, PPID: %d\n", getpid(), getppid());
9
10 sleep(3);
11 }
12 return 0;
13 }
通过命令查看进程,我们只看下两行,子进程PID对应没问题,值得注意的是父进程PPID
20825,对应的就是bash,bash对应的就是我们的shell,所以我们也知道shell本身也就是个进程。
命令行上启动的进程,一般它的父进程没有特殊情况的话,都是bash。
如果kill -9 20825,我们这整个shell就寄了,就需要重启恢复了。
我们重新启动程序,发现子进程PID变了,而父进程PPID依旧是20825。
我们的程序在运行后,其实就是shell的一个子进程,这个进程在这次结束后,再次运行,可能之前对应的PID就被其它进程继承了。(相当于医院挂号,如果你退出队列就可能需要重新挂号了)
那么为什么shell要创建子进程呢?
通过一个小程序理解一下
1 #include<stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 while(1)
7 {
8 printf("我是一个进程!, 我的ID是: %d, 父进程pid: %d\n", getpid(), getppid());
9 sleep(1);
10 int a = 1/0;
11 }
12 return 0;
13 }
通过运行,由于1/0的错误,命令行上启动的子进程终止,但这并不会影响bash,也就是说子进程的崩溃,不影响其父进程。
换句话来说,shell创建子进程,就是为了不影响父进程。
(就相当,你得去完成一件事,但是你不想因为做错这一件事背锅,所以你找了一个背锅的)
fork函数是一个系统调用,在当前进程下创建子进程,在被调用前只有一个父进程(当前程序),在调用后有一个父进程和一个子进程(新的进程)。
我们先来看功能:
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 fork();
7
8 printf("进程 PID: %d, PPID: %d\n", getpid(), getppid());
9
10 sleep(2);
11 return 0;
12 }
前面说到,fork在调用之前只有一个父进程,在调用后又多了一个子进程,所以结果打印了两句才是我们要的结果。
结果没问题。
父进程返回子进程PID,子进程返回0。
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 pid_t id = fork();
7 while(1)
8 {
9 if(id < 0)
10 {
11 printf("fork error!\n");
12 }
13 if(id > 0)
14 {
15 printf("父进程PID: %d, PPID: %d id: %d \n", getpid(), getppid(), id);
16 sleep(2);
17 }
18 else
19 {
20 printf("子进程PID: %d, PPID: %d id: %d \n", getpid(), getppid(), id);
21 sleep(2);
22 }
23 }
24 return 0;
25 }
从结果来看,在fork调用之前,进程21788执行代码,在fork调用之后,创建子进程,会有父进程+子进程两个进程在执行后续代码,后续代码被两个进程共享。
对于返回值,进程在执行fork后创建子进程,并且返回子进程的PID,创建的子进程调用fork返回0,通过返回值不同,让父子进程执行后续共享代码的一部分!
我们可能听过很多进程状态,比如:运行、挂起、等待、阻塞、挂机、死亡等,
进程的这么多状态,其实就是为了满足不同的运行场景的!
下面通过操作系统层面,理解一些重要的状态!
根据操作系统对硬件的管理(感性认知操作系统),我们得知操作系统需要对硬件进行管理。操作系统对每一个硬件都在内部加载了对应的结构体,里面有着硬件的各种属性,方便操作系统了解硬件的各种情况。
对于磁盘中的可执行程序,在加载到内存中,操作系统为了管理对应进程就有了对应的进程控制块。
根据冯诺依曼体系,上述结构体都在内存当中,因为操作系统在开机之后加载到了内存里。
如果当前进程想在CPU中运行,这时CPU就会为操作系统维护一个队列,这个队列其实就是为进程运行进行管理的(进程队列或进程排队)。
假设只有一个CPU,而一个CPU对应一个运行队列。
让一个进程进入CPU运行,就是让这个进程进队列,本质就是将该进程的task_struct 结构体对象放入队列中。(就相当于公司在面试时,在筛选简历后,是根据每个人正在排队的简历进行"运行")。
凡是在运行队列中的进程,它的状态都是运行状态(R状态)!
(不止是在CPU中跑,只要在队列里都算运行状态)
状态是什么?
状态其实就是进程的一种内部属性,进程的属性由进程控制块记录,而在进程控制块中其实就是数字
int ( 1 : run(运行),2:stop(中止),3:hup(挂起),4:dead(死亡)…)
根据冯诺依曼体系,CPU很快,相较于CPU外设很慢。
CPU运行进程时,可能你写的代码中需要对硬件进行访问(比如读写文件),所以进程或多或少都要访问硬件,而硬件不仅慢也是少量的,再者访问硬件的可能有很多进程(比如网卡需要对多个进程访问)。
当有个进程A正在访问磁盘,而进程B和C也需要访问磁盘,这时BC进程就需要等待!
所以不要以为,进程只会等待(占用)CPU资源,你的进程,也可能随时随地需要外设资源!
其实每个硬件在操作系统对应的结构体都会有自己的等待队列(wait_queue)。
当CPU在运行一个进程,这个进程需要访问磁盘,同时磁盘也正在被其它进程访问,需要这个进程等待,CPU不会跟着等待,通过操作系统改完相应状态数字,然后将这个进程(结构体变量)从运行队列放入磁盘的等待队列,而CPU继续跑其它进程。
进程在等待外设资源的状态,称为阻塞状态。
所谓进程的不同状态,本质是进程在不同的队列中,等待某种资源。
当磁盘准备好后,操作系统将进程对应状态改为R后,由CPU自动运行之后的代码。
进程的挂起状态是需要经过阻塞状态的。
如果当一个进程进入阻塞,进程放入硬件等待队列,此时进程被加载到内存,但是没有任何用处。
在此前提下,如果内存空间不够用了,操作系统就会在内存中保留进程控制块,将代码和数据放入磁盘中,这样原来的空间就可以提供给其它进程。(这样就完成了一个进程的挂起!)
进程挂起:进程暂时将代码和数据换出到磁盘。
所以阻塞不一定挂起,而挂起就一定阻塞,挂起是阻塞的一种状态。
以下是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 */
};
通过运行一个小程序,再查看它的进程状态。
1 #include <stdio.h>
2
3 int main()
4 {
5 int i = 0;
6 while(1)
7 {
8 i = 1+1;
9 }
10 return 0;
11 }
这样一个程序不断运行,相应状态也就对应R。
这里的+号代表前台进程,是可以通过ctrl+c终止的,如果不带+就是后台进程,只能通过kill中止进程。
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 int i = 0;
7 while(1)
8 {
9 printf("%d\n",i++);
10 sleep(3);
11 }
12 return 0;
13 }
有人可能疑问,这个程序明明一直在运行,为什么不是R状态,而是S状态呢?其实这个程序就体现了CPU和外设的速度差距,只要体会到CPU在等外设这点,就很好理解进程进入了S状态。
这个状态不好显示,但是我们可以谈谈它的场景。
D状态防止的场景:
进程A写入磁盘的过程中,磁盘(较慢)读取进程A的数据,进程A进入等待状态,此时内存由于负载太高导致操作系统需要删除一些不用的进程,此时进程A在操作系统看起来就是不用的进程,删除了等待状态的进程A,造成数据损坏。
赋予进程A深度睡眠状态,在该进程下无法被操作系统删除,只能断电或自己醒来才能解决。
对正在运行的进程
运用kill -19 PID
暂停进程
如果需要再次运行
用
kill -18 PID
继续进程
但值得注意的是,这里再继续进程的时候,进程就属于了后台进程。
这个暂停状态和上面暂停状态不同的是,上面的状态能通过Kill命令18信号进行唤醒,这个不会响应kill命令中的信号。
它还有另一种称号叫,正在被追踪状态,指的是进程暂停,等待跟踪它的进程对它的操作(比如当进程被调试的时候,等待gdb进程对它的操作)。
当process正在gdb调试中
还有Z、X状态我们放在下面开始。
当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
一个进程被创建出来,是为了完成任务的。
操作系统要知道它完成的如何,所有进程在退出时,不能立即释放其资源,需要保存一段时间,让OS或其父进程来读取。
通过一个小程序。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4
5 int main()
6 {
7 pid_t id = fork();
8 if(id == 0)
9 {
10 printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());
11 sleep(5);
12 exit(1);
13 }
14 else
15 {
16 while(1)
17 {
18 printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
19 sleep(1);
20 }
21 }
22
23 return 0;
24 }
代码解释:
利用fork调用,运行两个进程,当子进程结束,让父进程一直运行什么也不做,这时父进程就不能读取其子进程。
运行结果
再通过一个脚本持续查看进程状态
while :; do ps ajx | head -1 && ps ajx | grep myprocess | grep -v myprocess.c | grep -v grep; sleep 1; done
子进程正常退出死亡,但是资源没有被释放,处于Z状态。
只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
在子进程正常退出,并且资源被释放后,就是X(死亡)状态。
处于僵尸状态的进程,已经退出但是资源没有被释放完全,如父进程永远不回收,子进程资源永远占用内存就成了内存泄漏。(进程中的代码和数据可能释放,对应的PCB可能会被保留下)
下面通过一个程序认识孤儿进程
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 pid_t id = fork();
7 if(id == 0)
8 {
9 while(1)
10 {
11 printf("我是子进程, pid: %d, ppid: %d\n", getpid(), getppid());
12 sleep(1);
13 }
14 }
15 else
16 {
17 while(1)
18 {
19 printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
20 sleep(1);
21 }
22 }
23
24 return 0;
25 }
上述代码利用fork系统调用,让父子进程不断运行。
当进程运行的时候,我们通过kill -9 27455 杀死父进程。
通过杀死父进程,被bash回收,当前的进程被1号进程(init进程)领养,而一旦被领养就成了后台程序,只能被kill杀死。
父进程先退出,子进程就称之为“孤儿进程”
子进程为什么会被操作系统领养?
很简单,如果不领养,子进程在退出后就成了僵尸进程,没有进程来回收。
本章完~