在学习操作系统的过程中,我们常常能听到进程这一概念以及相关的一些知识。例如什么是父进程,什么是子进程,如何创建子进程,如何杀死进程等。了解进程之前,先来看一句话:进程是计算机科学中最重要和最成功的概念之一——————《深入理解计算机系统》。
第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。
对上层用户来说:进程就是程序的一个执行实例,正在执行的程序
对操作系统内核来说:进程就是担当分配系统资源(CPU时间,内存)的实体
如果一个程序需要运行,就需要将这个程序加载到内存,因为只有CPU才有计算能力,所以此时我们就应该将加载的程序称为进程
在生活中,一个人无论做什么,都需要进行先描述。例如在家中做饭:首先需要明确弄什么菜,有什么菜,怎么做。其次就是再组织:对这些菜进行加工,最终完成美味的菜肴。
对于进程而言:也有相关描述进程的信息,也可以称之为进程的属性。除此之外,这些信息不可能散落在内存当中吧,所以就需要将所有的信息按照一定的方式放在一起。操作系统就将进程的信息放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。这就是PCB(process control block),Linux操作系统下的PCB是task_struct。task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
task_ struct内容分类
进程的信息可以通过/proc系统文件夹查看
大多数进程信息同样可以使用top和ps这些用户级工具来获取
例如我们写的c程序如下:
#include
#include
#include
int main()
{
while(1)
{
printf("hello\n");
sleep(1);
}
return 0;
}
运行这个程序后,用以下指令查看
也可以通过系统调用获取进程标识符
进程id:(PID)
父进程id:(PPID)
C代码如下:
#include
#include
#include
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
先来看这样一段代码:
#include
#include
int main()
{
printf("hello fl\n");
int ret = fork();
if(ret > 0)
{
printf("hello if\n");
}
else
{
printf("hello else\n");
}
sleep(1);
return 0;
}
从语言角度来看,以上代码中 if 和 else 只会执行一个,但事实往往出乎意料。为什么会出现这种看似不合常理的结果呢,这就需要我们了解fork函数。
fork函数是Linux中一个重要的函数——它是从已经存在的进程(假设为p进程)中创建一个新进程(假设为c进程),p进程为父进程,c进程是子进程。上面之所以 if 和 else 都被执行了,是因为有两个进程。那么又有人疑惑,那为什么"hello fl"只被打印了一次,不是说有两个进程吗?说到这里先来想一个问题,首先看看一下代码:
int main()
{
......
fork();
......
}
假设以上代码从main函数开始执行,执行到fork函数时,创建子进程,创建出来的子进程又从main函数开始执行,又执行到fork函数,再次创建子进程……就这样无穷无尽,最终会导致系统挂掉,所以这是不合常理的。这只是我们的猜想,但事实也是类似的。对于fork函数,子进程应该是在fork函数之后开始执行的,因为默认情况下子进程会继承父进程的代码和数据,内核数据结构task_struct也会以父进程为模板,初始化子进程的task_struct,这其中就包括程序计数器。这就是子进程是从fork函数之后开始执行的根本原因(fork之前的代码和数据也是被共享的,但只是不执行罢了)。
说到这里,有人可能又会问:只打印一个"hello fl"我明白了,如果 if 为父进程(子进程)的结果,那么 else 为子进程(父进程)的结果,这我也能明白,但为什么 if 和 else分别执行了一次,为什么不是两次都执行 if 或者两次都执行 else?这其实是与fork函数的返回值有关:
1.在父进程中,fork返回新创建子进程的进程ID
2.在子进程中,fork返回0
3.如果出现错误,fork返回一个负值
前面说到子进程会继承父进程的代码和数据,以及task_struct,这就相当于子进程是父进程的拷贝或者复制,难道我们花了时间和精力就是为了得到一个副本,然后做相同的事?这对于很多情况来说,是完全没有意义的。因此可以根据返回值的不同,创建子进程就可以通过 if else 分流来让父子进程做完全不同的事 。这也就是创建子进程的意义——分摊压力/做其他的工作
如何理解fork的返回值?
一个父进程可能存在多个子进程,为了明确当前被创建的子进程是哪一个,所以返回子进程的ID,而一个子进程只可能有一个父进程,而子进程可以通过调用getppid()
来获取父进程的ID,所以在子进程中fork返回0
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4 int main()
5 {
6
7 int ret = fork();
8 if(ret == 0)
9 {
10 printf("child : %d ret :%d\n",getpid(),ret);
11 }
12 else if(ret > 0)
13 {
14 printf("parent: %d ret :%d\n",getpid(),ret);
15 }
16 else
17 {
18 perror("fork");
19 return 1;
20 }
21 sleep(1);
22 return 0;
23 }
关于fork之后,父子进程谁先运行?
这是不确定的,因为这跟调度器有关,如果调度器想让父进程先执行,那么父进程先执行,反之子进程先执行
在Linux操作系统中,每个进程都有不同的状态,一个进程在一个时间段内可能有多个状态
了解进程状态的时候,我们需要先了解几个知识:
1.进程的信息状态在哪里? 在task_strcut(PCB)中
2.进程状态的意义? 方便OS(操作系统)快速判断进程,完成特定的功能,比如调度
R运行状态(running)
一个进程处于运行状态,一定占有CPU吗?答案是不一定的。在Linux下,在该状态下才可能在CPU上执行,换句话说,处于该状态下,并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列(进程的task_struct结构(进程控制块)被放入对应CPU的可执行队列中)。进程调度器的任务就是从各个CPU的可执行队列中分别选择一个进程在该CPU上运行。
S睡眠状态(sleeping)
假设今天早上起床,我想泡杯咖啡,所以我就去烧水,因为烧水需要一定的时间,所以我就需要等待水被烧开,然后才能泡咖啡。
进程也是类似的。如果一个进程需要的资源(除CPU之外)得不到满足时,进程就会进行等待,该进程的task_struct结构被放入对应事件的等待队列中,这个过程我们称之为挂起(阻塞)。等待队列中的进程都不会被调度,除非进程的task_struct结构被放入运行队列中,然后就能被CUP调度,这个过程我们称之为唤醒。
D磁盘休眠状态(Disk sleep),有时候也叫不可中断睡眠状态(uninterruptible sleep)
与S状态类似,进程处于睡眠状态,但是此时进程是无法中断的。即使使用kill -9也无法杀死处于D状态的进程。在这个状态的进程通常会等待IO的结束。
T停止状态(stopped)
可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。与S状态有些类似,但处于S状态的进程,虽然代码没有跑,什么事都没干,不过一些核心的数据是会被更新的,比如进程睡眠的时间,而T状态是彻底暂停,不会再有数据的更新。
僵死状态(Zombies)
进程在退出的过程中,处于Z状态。
在这个退出过程中,进程占有的所有资源将被回收,除了task_struct结构(以及少数资源)以外。于是进程就只剩下task_struct这么个空壳,故称为僵尸。之所以保留task_struct,是因为task_struct里面保存了进程的退出码、以及一些统计信息。而其父进程很可能会关心这些信息,因为可能需要了解子进程是正常退出,或者异常终止等。
X死亡状态(dead)
处于X状态表明该进程的所有资源(进程相关的内核数据结构,代码以及数据)都已经被回收。这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
1 #include<iostream>
2 #include<unistd.h>
3 using namespace std;
4
5 int main()
6 {
7 pid_t id = fork();
8 if(id == 0)
9 {
10 //child
11 while(true)
12 {
13 cout << "I am child, running" << endl;
14 sleep(2);
15 }
16 }
17 else
18 {
19 cout << "father do nothing" << endl;
20 sleep(50);
21 }
22 return 0;
23 }
僵尸进程的危害:
1.进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
2.维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
3.那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
4.内存泄漏
孤儿进程
孤儿进程与僵尸进程在理解上可以认为相反。
父进程先于子进程退出,父进程退出后,子进程成为后台进程,并且父进程为1号进程。
当我们杀死父进程时,父进程已经挂掉了,但子进程还在继续运行,此时子进程就会被1号进程领养,那么这种进程我们称之为孤儿进程。领养的目的在于,如果子进程不在运行,就由1号进程回收其资源。1号进程也就是操作系统
基本概念
在Linux下通过ps -l
命令查看进程的几个重要信息
UID:代表执行者的身份
PID:代表这个进程的代号
PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI(调度优先级):代表这个进程可被执行的优先级,其值越小越早被执行
NI(普通优先级):代表这个进程的nice值
PRI and NI
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小
进程的优先级别越高
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
所以,调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是-20至19,一共40个级别
PRI vs NI
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进
程的优先级变化。
可以理解nice值是进程优先级的修正修正数据
用top命令更改已存在进程的nice
1.输入top值
2.进入top后按 r -》然后输入进程PID -》输入nice值 -》回车
进程具有三个重要的性质:独立性、动态性、并发性
独立性
:经常在操作系统中独立存在,拥有独立的资源和私有的地址空间。没有经过进程自身允许,其它用户进程不能直接访问进程的地址空间。
动态性
:进程是运行中的程序,具有自己的生命周期和各种不同状态。
并发性
:多个进程可以在单个处理器上并发执行,互不影响。所谓并发(concurrency)指的是同一时刻只能执行一条指令,但多个进程可以快速的切换执行,使得宏观上具有多个进程同时执行的效果,这种交错执行称为上下文切换。并行(parallel)则指同一时刻有多条指令在多个处理器上同时执行
————来自《深入理解计算机系统》