进程的概念
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
进程的概念
- 一、进程
- 二、进程控制块
- 三、获取进程标识符
- 四、 进程状态
- 1、R状态
- 2、S状态
- 3、D状态
- 4、T状态
- 5、t状态
- 6、X状态
- 7、Z状态
- 孤儿进程
- 五、进程优先级
一、进程
进程是什么东西呢 ?
和我们的程序有什么关系和区别呢 ?
上面的.exe 是不是一个应用程序啊,后面写着,但是当我们双击这个应用程序的时候,这时候这个程序就变成了一个进程,开始运行了起来。
我们来看看程序和进程的定义
程序:程序是一组预先编写好的指令,用于执行特定任务。它是一个静态实体,存储在磁盘上,直到被加载到内存中才能执行。
进程:进程是程序的一次执行实例。当一个程序被加载到内存并开始运行时,它就变成了一个进程。进程是一个动态实体,会随着时间和环境的变化而变化。
这样一来再看定义是不是就清晰了一点。最后在总结一下,进程是程序的实体。程序要运行,首先得加载到内存中,而正在运行的程序就叫做一个进程,操作系统中会同时运行很多个进程 。(由第一个图可以看出来)
二、进程控制块
先说一说操作系统
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。
但是为什么要有操作系统呢,设计操作系统的目的又是什么呢 ?
- 与硬件交互,管理所有的软硬件资源
- 为用户程序(应用程序)提供一个良好的执行环境
操作系统是一款搞管理的软件。
既然操作系统是管理的软件,那操作系统是如何管理进程的呢 ?进程的信息又放在了什么地方 ?
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
在课本上是叫作PCB,Linux下的PCB叫作task_struct 。它的内部包含了进程的所有信息,包括进程的编号、状态、优先级、程序计数器、上下文数据等等。
进程的信息可以通过 /proc 系统文件夹查看
要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。
查看正在运行的进程信息,用 top或者ps 命令
这里是我开了两个窗口,快捷键(Ctrl+shift+n),一个窗口让一个进程运行,一个窗口查看进程是否在运行中,这里可以看到是可以查到的,用 ps 命令。
其中,PID是自己的进程标识符,每个进程都有一个唯一的标识符用于区分。PR是优先级,NI是nice值,S是状态。
三、获取进程标识符
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1)
{
printf("pid: %d\n", getpid()); //获取自己的pid
printf("ppid: %d\n", getppid()); //获取自己父亲的pid
sleep(1);
}
return 0;
}
可以对应着看一下,pid 和 ppid 都是可以对的上的吧。
fork方法创建子进程
通过 man fork 可以查看。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
printf("hello : %d!, ret: %d\n", getpid(), ret);
sleep(1);
return 0;
}
一个 printf 语句竟然执行了两条。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
if(ret < 0){
perror("fork()");
return 1;
}
else if(ret == 0){ //child
printf("child : %d!, ret: %d\n", getpid(), ret);
}else{ //father
printf("father : %d!, ret: %d\n", getpid(), ret);
}
sleep(1);
return 0;
}
子进程可以成功进入if 分支,说明它是与父进程共享代码和数据的!一般而言子进程创建出来后就会与父进程共享后面的代码和数据,包括return语句,所以会返回两次。返回不同的值,也是为了区分父子进程,让不同的执行流来执行不同的代码块。
但是进程是有独立性的。子进程和父进程共享代码可以理解,因为程序运行中代码是不能被修改的,但是数据是可以被修改的,如果父子进程共享数据的话,那么一个进程修改数据之后不会影响到另一个进程吗?
这里牵扯到另一个概念,即子进程的写时拷贝。父子进程最初共享同一份数据,如果此时子进程要修改父进程的数据,那么就会发生写时拷贝,即给子进程分配一块新的空间并拷贝原数据,然后进行修改。相比一开始就把全部数据拷贝一份,这种方法能够很好的减少空间浪费。
所以这也解释了为什么变量id能够同时有两个不同的值。
四、 进程状态
进程状态可以反应进程在执行过程中的变化,在操作系统中进程状态可以分为五个基本状态,即新建状态、运行状态、就绪状态、阻塞状态、终止状态。
其中最主要的是运行态、就绪态和阻塞态。当一个进程在CPU中被执行时处于运行态,但是操作系统中有很多个进程需要运行,不能让一个进程占用CPU过长时间,于是需要按照时间片来排队执行。当一个进程的运行时间片到了,就需要退出运行态,重新等待CPU的调度,变成就绪态。如果一个进程在执行期间需要等待某个事件,例如进程要访问外设,但是外设没有输入时,进程就要进入外设的等待队列,此时就会进入阻塞态,等待事件的完成。当外设输入后,进程重新回到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 */
};
- R(running):运行状态,进程在被CPU调度或处于运行队列中
- S(sleeping):睡眠状态,进程在等待时间完成,也叫做可中断睡眠状态
- D(disk sleep):磁盘休眠状态,也叫不可中断睡眠状态,该状态下的进程会等待IO的结束
- T(stopped):停止状态,可以通过发送信号让进程停止
- t(tracing stop):追踪状态,程序在调试时遇到断点停下的状态
- X(dead):死亡状态,进程死亡后被回收的状态
- Z(zombie):僵尸状态,进程死亡后未被回收时保持的状态
1、R状态
#include <stdio.h>
int main()
{
while(1)
{
;
}
return 0;
}
可以看到此时进程的状态就是R+,这个+代表进程此时在前台运行,如果我们想让进程在后台运行的话,只需要在运行程序时在命令的后面加上 & 即可,此时就可以在运行程序的同时输入其他命令了。
2、S状态
#include <stdio.h>
int main()
{
int a = 0;
scanf("%d",&a);
return 0;
}
可以看到此时进程状态就是S+,进程正在等待外设的输入。
3、D状态
在操作系统的内存处于危急存亡的时刻,将进程挂起也不管用了,这时候操作系统就会开始杀进程。
但是假如一个进程正在向磁盘中写入一些十分重要的数据,在等待磁盘写入完毕的期间会处于阻塞态,如果此时被操作系统杀掉了,就会导致数据丢失进而产生严重的后果。因此针对这种情况设计了深度睡眠状态即D状态,位于该状态的进程无法被操作系统杀掉。
4、T状态
可以看到18号信号SIGCONT是继续进程,19号信号SIGSTOP是暂停进程。
Ctrl + z 对应19 号信号。
5、t状态
可以看到,我们在程序中打了一个断点并开始运行程序,当运行到断点处时程序暂停,此时观察进程状态就会发现该进程正处于t状态。
6、X状态
进程死亡后的状态,这个状态持续的事件十分短暂,我们几乎无法观察到。
7、Z状态
实际上,进程死亡后并不是立即就变为X状态了,而是需要先经过Z状态即僵尸状态。
就是孩子进程结束的时候,父亲没有回收的时候,这个时候就会造成僵尸进程,当一个进程死亡后,需要维护自己的相关资源等待父进程回收并提交进程的死亡信息。如果父进程迟迟不读取子进程的状态,子进程就会一直保持Z状态不能被释放。
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t ret = fork();
if(ret < 0){
perror("fork()");
return 1;
}
else if(ret > 0){
printf("child : %d!, ret: %d\n", getpid(), ret);
sleep(30);
}else{
printf("father : %d!, ret: %d\n", getpid(), ret);
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态 ? 答案是对的。
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构
对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
孤儿进程
前面提到,子进程先结束而父进程迟迟不结束,子进程会保持僵尸状态。
如果父进程提前结束,而子进程还在运行,那么子进程就变成了孤儿进程。当子进程结束之后该由谁来回收呢?
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t ret = fork();
if(ret < 0){
perror("fork()");
return 1;
}
else if(ret > 0){
printf("child : %d!, ret: %d\n", getpid(), ret);
sleep(5);
}else{
printf("father : %d!, ret: %d\n", getpid(), ret);
sleep(30);
}
return 0;
}
可以看到,父进程退出后子进程的PPID变为了1,即我们的操作系统。
所以当父进程比子进程先结束时,子进程就会被操作系统领养,最后由操作系统负责回收。
五、进程优先级
cpu资源分配的先后顺序,就是指进程的优先权。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的 linux 很有用,可以改善系统性能。
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值