基本概念:
- 进程是程序的一个执行实例;
- 从内核来看,进程是担当分配系统资源的实体。
注:在Linux操作系统中,大多数指令都是创建了一个个的进程。
操做系统如何管理内存?
答: 使用一个结构体(PCB)来描述进程;
使用高效的数据结构来组织进程。
描述进程--PCB(process control block):
进程的信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。这个数据结构在Linux操作系统中被叫做task_struct。
存放进task_struct结构体中的信息有:
- 标识符:用来区别其他进程的唯一标识符;
- 状态:任务的状态,退出代码,退出信号等;
- 优先级:相对于其它进程的优先级;
- 内存指针:是用来存放代码和数据的地址,还有其他进程共享的内存块的指针;
- 程序计数器:程序中即将被执行的下一条指令的地址;
- 上下文:进程执行时寄存器中的数据(保护现场,进程切换后,切换回来的时候方便恢复到切换时的进程执行的状态);
- I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表;
- 记账信息:可能包括处理器的时间总和,使用的时钟数总和,时间限制和记账号等。
- 其它信息。
组织进程:
使用一个代替头结点的带环的双向链表来组织进程。
创建一个进程的本质是:创建一个task_struct结构体,并将该结构体进行初始化插入上面说的带头结点的双向带环链表。
查看进程:
- 若要查看pid为1的进程,需要查看/proc/1文件夹
ps aux(查看所有进程)
ps aux | grep hello (用ps aux的输出来作为grep的输入)
int main()
{
while(1)
{
sleep(1);
}
return 0;
}
通过系统调用获取进程标识符(PID):
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid = getpid(); //获取当前进程标识符
pid_t ppid = getppid(); //获取当前进程的父进程的标识符
printf("pid:%d ppid:%d\n", pid, ppid);
}
通过系统调用创建进程--fork:
通过fork系统调用可以创建一个子进程,通过在Linux操作系统上使用man fork学习fork,我们可以总结出以下结论:
- fork创建进程以原进程为模板,复制PCB的内容,拷贝代码和数据(共用一份代码,数据各自开辟空间,私有一份。采用写时拷贝)。
- 调用一次fork有两个返回值,在父进程和子进程中分别返回;父进程中返回子进程的PID,子进程中返回0.
编译运行下面的代码,可以看到父进程的中的返回值恰好是子进程的PID:
int main()
{
pid_t ret = fork();
if(ret > 0)
{
printf("father:%d ret:%d\n", getpid(), ret);//father
sleep(1);
}
else if(ret == 0)
{
printf("child:%d ret:%d\n", getpid(), ret);//child
}
else
{
perror("fork\n");
}
}
- 父子进程的执行顺序不一定,取决于操作系统调度器;
- 父子进程都从fork执行结束的位置开始执行;
- fork失败的原因主要有:内存不足和创建的进程达到数目上限。
进程状态:
R(running):运行(就绪状态),不一定正在运行,也可能是在运行队列中;
S(sleeping):睡眠状态,意味着进程正在等待事件完成,在这个状态是可以打断的;
D(disk sleeping):磁盘休眠状态,也叫不可中断睡眠状态,该状态通常会等待IO的结束;(当密集的读写IO设备时会出现这个状态)
T(stopping):停止状态,可以通过SIGSTOP信号来停止进程,通过SIGCONT信号来让进程继续运行;
下面一段代码可以使进程一直处于运行状态,
#include <stdio.h>
int main()
{
while(1)
{
;
}
return 0;
}
然后我们使用信号来使进程停止:
也可以使用信号来恢复:
t(tracing stop):跟踪状态;
X(dead):理论上存在;
Z(zombie):僵死状态。
僵尸进程:
僵死状态是一个特殊的状态,当子进程退出而父进程未能读取子进程的返回值,就会产生僵死状态(进程)。这时候僵尸进程会一直等待父进程读取返回代码。
所以,产生僵尸进程的条件是子进程退出,父进程还在运行同时未读取子进程的返回状态,子进程进入僵尸状态。
创建一个僵尸进程:
int main()
{
pid_t ret = fork();
if(ret > 0)
{
while(1)
{
printf("father:%d ret:%d\n", getpid(), ret);//father
sleep(1);
}
}
else if(ret == 0)
{
printf("child:%d ret:%d\n", getpid(), ret);//child
sleep(1);
}
else
{
perror("fork\n");
}
}
僵尸进程的危害:
1 进程的退出状态必须被维持下去,因为他要告诉父进程,你交给我的任务完成的怎么样了。如果父进程一直不读取,子进程就会一直处于Z状态。
2 维护进程的退出状态要用数据维护,也是进程的基本信息,保存在(task_struct)PCB中;也就是说,如果Z状态一直不退出,PCB就要一直维护。
3 如果一个父进程建立了许多子进程而不进行回收,就会造成内存资源的浪费。因为保存进程信息需要结构体PCB,定义结构体需要在内存中开辟空间,又不进行回收,就会造成内存资源的浪费,造成内存泄漏。
孤儿进程:
如果父进程提前退出,那么这时候子进程就会成为孤儿进程,孤儿进程会被1号进程领养。
下面来模拟实现一个孤儿进程:(3秒后父进程退出,子进程被1号进程回收)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t ret = fork();
if(ret > 0)
{
//father
printf("father:pid %d ppid:%d ret:%d\n", getpid(), getppid(), ret);
sleep(3);
}
else if(ret == 0)
{
//child
while(1)
{
printf("child: pid %d ppid:%d ret:%d\n", getpid(), getppid(), ret);
sleep(1);
}
}
else
perror("fork");
return 0;
}
值得注意的是,孤儿进程不是状态,而僵尸进程是一个状态。
进程优先级:
基本概念:
- CPU资源分配的先后顺序,就是指进程的优先权(priority)
- 优先权高的进程有优先执行权利。配置进程优先级对任务环境的Linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样,把不重要的进程安排到某个CPU,可以大大改善系统性能。
查看优先级:
ps -l
ps -al
ps -elf
ps elf
PRI是进程的优先级,数值越小,优先级别越高。
NI是nice值,表示进程可被执行的优先级的修正数值。
PIR(new)=PIR(old)+nice值,当nice值为正时,优先值越大,优先级别越小;当nice值为负时,优先值越小,优先级别越大,越快被执行。
NI的取值范围是-20~19,共40个数据。
修改进程的优先级:
可以使用nice和renice命令修改进程优先级。
启动进程前调整:nice -n -5 ./hello
调整已经存在的进程:renice -5 -p 3498 //将PID为3498的进程的nice值改为-5
用top查看进程优先级并修改进程优先级:
输入top回车->进入top后按“r”->输入进程PID->输入nice值
注:
-
进程数目众多,而CPU只有少量甚至只有1个,所以为了高效完成任务,进程具有竞争性,它们要合理竞争相关资源,这样就有了优先级;
-
多进程运行,需要独享各自的资源,做到进程运行期间互不干扰,进程具有独立性;
-
多个进程在不同的CPU下同时运行,叫做并行;
-
多个进程在一段时间内在同一CPU下用切换的方式共同推进,由于这个时间非常短,人一般察觉不到,就像进程是同时发生的一样,叫做并发。