目录
一、fork创建子进程
1.1运行 man fork 认识fork
fork有两个返回值 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)。
可以看到程序执行了两边after fork,这是因为在fork处创建了一个子进程,子进程有和父进程一样的代码从fork开始,子进程开始执行而父进程还会继续往下执行,所以子进程执行了一遍printf,父进程也执行了一遍printf。
19517就是父进程的pid而19518就是子进程的pid,而15094则是bash
查看fork可以得知
fork成功后将子进程pid返回给父进程返回0给子进程,如果失败就返回-1.
通常我们可以通过下面的代码操作来对父子进程进行分流操作,让父子进程去执行不同的代码,实现不同的功能。
1.2fork原理及返回值
fork在创建子进程后,将子进程的pid返回给父进程,将0返回给子进程,这是因为父子进程之间是1:n的关系,一个父进程可以创建多个子进程,而每个子进程只能对应一个父进程,而pid标识符具有唯一性,所以父进程想要找到具体的子进程就必须要有子进程的pid。
而进程之间都具有独立性,不能相互影响,OS在设计时就必须保证这一点,所以父子进程之间也不能相互影响,子进程继承父进程的代码和属性,当子进程尝试对代码进行修改操作时就会发生写时拷贝。
二、进程状态
既然有了父子进程,它们各自执行,那进程在执行时都处于什么样的状态呢?
2.1排队
拿以上代码举例,当程序执行到scanf时,就会停下等待输入。
所以进程并不是一直在运行的,它可能在等待某种硬件资源。
即使进程放在了CPU上,也不是一直会运行的,每个进程都有一个时间片,所以在日常写代码过程中,即使我们写了死循环也不会把操作系统搞挂。
进程=内核数据结构(task_struct)+可执行程序。而所谓的进程的排队,并不是进程本身的可执行程序在排队,而是task_struct即PCB在排队。
而一个task_struct可以被链入多种数据结构中,而在Linux内核中,它不是被链入到一个单链表当中的而是被链入到一个双链表中的,如下图所示
每个task_struct中都有一个类似于struct_listnode的结构体,里面存放的就是这个PCB的上一个地址和下一个地址。同样的指针指向的也不是其他task_struct本身而是其他struct_listnode的地址。可是怎么通过这些地址来拿到task_struct中的其他数据呢?
小到整形大到结构体,在拿到一个地址要对整个数据访问时,都会涉及到一个名为偏移量的东西,拿上图举例,操作系统在进行存储时,是从低到高进行空间的使用的,先使用低地址,再使用高地址,所以&n=&t+偏移量,现在我们知道&n想要拿到&t,那么&t=&n-偏移量就可以得到初始地址。
偏移量=(task_struct*)0->n;
起始地址=(task_struct*) (&n-&((task_struct*)0->n));
而一个task_struct中也不止存在一个这样的struct_listnode。此时就可以做到,每一个PCB即可以被链入到全局双链表也可以被链入用户想链入的任意一个队列中。所以进程的增删就转变成对链表的增删。
2.2状态
1、 所谓的状态,本质就是一个整形变量,在task_struct中的一个整形变量。
所谓的状态就是task_struct中status的值对应上面的1,2,3。
2、状态决定了该进程的后续动作
Linux中可能存在多个进程都要根据它的状态执行后续动作。
一个CPU维护一个运行队列
2.3运行、阻塞、挂起
操作系统管理对应的硬件都是先描述再组织。
所以类似于scanf之类的进行执行时,该进程就会被链入硬件的链表,状态改为阻塞,并且将该进程PCB从运行队列中剥离下来投递到底层的等待队列中。
而硬件的就绪状态只有操作系统最清楚。当键盘完成输入操作后,操作系统就会再次把该进程链入到运行队列中,将其状态改为运行状态,继续让CPU去调度向后执行。
所以所谓的状态变更本质上就是把进程投递到不同的队列中。
而挂起状态有个前提:计算机资源(内存)较为吃紧。
例如在阻塞状态下,把进程相关的代码数据交换写入到外设例如磁盘当中,而在磁盘中也存在一个swap分区专门用来在操作系统内存吃紧时和操作系统进行数据换入和换出的,从而释放对空间的部分压力。此时一旦进程所对应的数据不在内存当中,此时我们就将该进程称为挂起状态。(注意,PCB即task_struct是不会被换出的 )所以也可以得知,在创建一个进程时一定是先创建内核数据结构即PCB的。
三、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 */
};
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠也称为浅度睡眠)通常就是所谓的阻塞状态。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束,D状态也是一种阻塞状态。
T停止状态(stopped): kill-19可以通过发送 SIGSTOP 信号给进程来停止(T)进程,进程被停止后会由前台进程转变为后台进程。kill-18这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。同样在gdb调试时也可以让程序进入T状态此时是一种tracing stop状态。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态 。
Z状态也叫僵尸状态:是指进程已经死亡(结束)代码和可执行程序已经被释放,但是PCB内核数据结构即当前状态要维持住,供上层读取,此时它的PCB在被父进程读取之前会被操作系统保存,此时就是僵尸状态,如果父进程不读取,那么这个僵尸状态将会一直存在,此时就会产生内存泄漏。
状态演示
睡眠和运行
STAT下面所表示状态后面的加号表示其是一个前台进程,如果在运行可执行文件时在后面带上&符号,操作系统就会在执行时将其变成后台程序,此时想要杀死进程Ctrl C已经不能做到了,此时只能使用kill-9强行杀掉进程。
僵尸状态
通过上图代码,我们让子进程运行5秒后提前结束,但是父进程继续运行,不去读取子进程信息,此时子进程就是Z状态。
那么如果此时修改代码,让父进程比子进程先结束会发生什么呢,这里代码就不进行演示了,直接看结果:
父进程提前结束后,子进程的ppid就变成了1,这个1就是操作系统,而Linux中对这个现象做出非常形象的解释,此时这个继承的父进程提前结束,而子进程被操作系统领养,此时这个子进程就被称为孤儿进程,而在它变成孤儿进程的同时,它也会变成后台进程,只能用kill-9杀掉.