Fork me on GitHub

Linux_进程概念

进程概念

进程初识
  • 什么是进程

进程就是运行起来的程序:
一个具有一定独立功能的程序在数据集合上的一次动态执行过程

  • 每个进程都有自己的状态

  • 每个进程都有自己的虚拟地址空间

  • 进程是操作系统分配资源的基本单位

  • 如何管理进程

操作系统要对进程进行管理

操作系统对进程的描述:PCB(进程控制块),在Linux下面叫task_struct

  • PCB(进程控制块)

进程信息被放在一个叫进程控制块的数据结构中,Linux中的PCB是:task_struct,它里面包含着进程的信息,并且会被装载到RAM内存中

task_struct的源码参考:https://elixir.bootlin.com/linux/latest/source/include/linux/sched.h

  • 操作系统对进程的详细描述
1
2
3
4
5
6
7
8
9
10
11
标识符(pid):描述本进程的唯一标识符,用来区别其他进程
状态:任务状态,退出代码,退出信号等
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块指针
I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用文件列表
记账信息:可能包括处理时间总和,使用的时钟数总和、时间限制、记帐号等
优先级:相对于其他进程的优先级
多任务,多用户操作系统
分时机制---时间片
多个进程在CPU上进行切换运行:
程序计数器----用于保存程序运行的下一条指令
上下文数据----用于保存进程切换时寄存器上的数据
如何查看一个进程
  • 查看一个进程
1
2
3
4
5
6
   可以通过 /proc 系统文件查看	
ps -ef 查看进程信息
ps aux 显示信息更全面(所有进程)
ps aux | grep
top 查看系统资源
getpid() 这是一个系统调用接口,在代码中获取进程pid

avater

  • 通过系统调用接口获取pid(进程ID)和ppid(父进程ID)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*调用函数所要的头文件,和函数接口
#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);
pid_t getppid(void);

*/
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>

int main()
{
printf("get pid:%d\n",getpid());
printf("get ppid:%d\n",getppid());
return 0;
}

avater

如何创建一个进程
  • 创建一个进程的一般步骤
  1. 分配一个PID(从小到大找第一个没有被使用的ID)
  2. 分配PCB,拷贝父进程的PCB的绝大部分数据
  3. 给子进程分配资源
  4. 复制父进程地址空间
  5. 将子进程制成就绪状态放入就绪队列
  6. 调用pid_t fork(viod)函数
  7. 返回值:父进程来说,返回值是子进程的pid,对于子进程来说,返回值是0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//这是一个创建一个进程的代码
//调用fork()函数

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>

int main()
{
pid_t pid = fork();
if(pid < 0) {
printf("fork error\n");
return -1;
}
else if(pid == 0){
printf("this is child %d\n",getpid());
}
else{
printf("this is parent %d, child:%d\n", getpid(),pid);
}
printf("pid : %d\n",pid);
while(1){
sleep(1);
}
return 0;
}

avater

  • 使用fork系统调用通过复制调用进程来创建一个子进程,调用进程我们称之为父进程,创建出来的新进程称之为子进程
  • 因为子进程是以父进程为模板来创建的,因此父子进程的代码段是完全一样的,也就是说,他们运行的程序是一样的
  • 父子进程返回值不同,父子进程共用同一个代码段,但是他们的数据并不共用
  • 我们用户就是通过对返回值的判断来分辨父子进程,来进行代码分流
  • fork产生新任务的速度非常快,因为fork与原任务共享一个写时复制的内存空间
  • 所谓写时复制就是两个任务可以同时自由的读取内存,但任意一个试图对内存进行修改时,内存就会复制一份提供给修改方单独使用
进程状态
字母 状态
R 运行状态(running)
S 睡眠状态(sleeping)可中断睡眠态
D 磁盘睡眠状态(disk sleep)不可中断睡眠态
T 停止状态(stopped)
t 追踪态(tracing stop)
X 死亡状态(dead)
Z 僵尸状态(zombie)
僵尸进程

产生原因:子进程先于父进程退出,他要保留退出原因在PCB中,因此退出后不会释放所有资源,子进程退出后操作系统会通知父进程这个家属说子进程退出了,你去获取以下原因,然后完全释放资源。假如父进程不管子进程的退出状态,那么这个子进程将进入僵死状态,成为僵尸进程。

危害:资源泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//实现一个僵尸进程,十秒之后子进程退出
//僵尸进程:子进程先于父进程结束

#include <stdio.h>
#include <unistd.h>
#include <error.h>
#include <stdlib.h>

int main()
{
pid_t pid = fork();
if(pid < 0){
perror("fork filed:");
return -1;
}
else if(pid == 0){
printf("this is child!!!\n");
sleep(10);
exit(EXIT_SUCCESS);
}
else{
printf("this is parent~~!~~~~\n");
while(1){}
}
}
1
2
//用一个脚本文件来观察僵尸进程的产生
while :; do ps aux|grep zombie | grep -v grep;sleep 1; echo "#######################";done

avater

僵尸进程是不能用kill被杀死的,从系统中移除其的唯一方法就是杀掉他们的父进程(或等待其父进程终止),此时1号进程(init)将接管和等待这些僵尸进程,将它们清除掉。

  • 当创建的僵尸进程过多的时候会导致以下问题:
  1. 造成内存资源的浪费
  2. 内存泄漏
  3. 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,僵尸状态一直不退出,PCB一直都要维护
孤儿进程

父进程先于子进程退出,那么子进程将成为孤儿进程,并进入后台运行。

父进程变为init进程,也就是说子进程后来退出了,init进程将负责释放资源,init进程非常负责任,因此进程不会成为僵尸进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//实现一个孤儿进程
//孤儿进程:父进程先于子进程退出

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<error.h>

int main()
{
pid_t pid = fork();
if(pid < 0){
printf("fork error:");
return -1;
}
else if(pid == 0){
printf("this is child!!!\n");
sleep(15);
printf("init get child!!!\n");
}
else{
printf("i am parent~~~~~~~~\n");
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}

avater

avater

进程优先级
  • [ ] 为什么要有进程优先级?

  • 因为进程的功能各有不同,因此对CPU资源的要求也各有不同,因此针对进程的调度就有了优先级,优先级决定了CPU资源的优先分配权

  • PRI:进程可被执行的优先级,其值越小越早被执行
  • PRI = PRI + NI(进程的nice值)
  • 修改进程优先级的命令:nice/renice 通过设置NI的值来修改进程的优先级
  • nice取值范围:-20-19,总共40个级别
  • 用top命令来更改已存在的nice:top命令–>”r”–>输入PID–>输入nice值

avater

avater

avater

其它概念 简介
竞争性 系统进程数目众多,而CPU资源只有少量,甚至至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行 多个进程在多个CPU下分别同时运行,这称之为并行
并发 多个进程在一个CPU下采用进程切换的方式,在一段时间内让多个进程都得以推进,称之为并发
环境变量
  • 功能:用于在操作系统中设置环境参数的一些变量
  • 特性:这些环境变量具有全局特性
  • 环境变量相关命令:

env 显示所有环境变量

echo $NAME 显示某一个环境变量

export 设置环境变量

set 查看环境变量更加详细

unset 取消一个环境变量

  • 获取环境变量:
  1. extern char **environ;
  2. main函数第三个参数
  3. getenv(“PATH”);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//获取环境变量的三种方式

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

//方式一:通过全局变量
int main()
{
extern char** environ;
int i;

for(i = 0;environ[i] != NULL; i++){
printf("env:[%s]\n",environ[i]);
}

return 0;
}
//方式二:通过main函数的第三个参数
int main(int argc, char* argv[], char* env[])
{
int i = 0;

for(i = 0; env[i] != NULL; i++){
printf("ENV:[%s]\n",env[i]);
}
return 0;
}
//方式三:通过系统调用获取指定的环境变量
int main()
{
//char* getenv(const char* name);
char* path = getenv("PATH");
printf("env:[%s]\n",path);
return 0;
}
  • 本地变量和环境变量:

本地变量只能在当前进程使用,不能在子进程中使用

父进程定义的环境变量能在当前进程和子进程中使用

子进程定义的环境变量只能在子进程使用,不能在父进程中使用

程序地址空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <unistd.h>

int val = 100;
int main()
{
int pid = fork();
if (pid < 0) {
return -1;
}else if (pid == 0) {
val = 200;
printf("child val:%d ---%p\n", val, &val);

}else {
sleep(1);
printf("parent val:%d ---%p\n", val, &val);
}
return 0;
}

avater

通过结果中打印的地址相同,我们可以推断出:打印出的地址都是虚拟地址

  • 程序地址空间 —>进程地址空间 —>虚拟地址空间
  • 程序地址空间:进程的虚拟地址空间
  • 地址:指向内存区域的一个编号

avater

  • 虚拟地址空间:
  1. mm_struct这个结构体描述出了虚拟地址空间
  2. 我们获取到的地址都是虚拟地址,不是真正的物理内存地址
  3. 虚拟地址是通过页表转换映射之后得到物理内存地址进而访问到内存的
    让我们进程独立起来
  • 页表:记录虚拟地址和物理地址的转换关系
  1. 页表还具有内存访问控制的限制
  2. 页表中还记录了要访问的这块地址的属性
  • 内存访问控制:通过对虚拟地址的权限标志来实现对内存的访问控制
  • MMU:内存管理单元
  • 写时拷贝技术(父进程创建子进程)

avater

进程调度
  • 进程调度调度的是基于PCB调度的

  • 常见的调度算法

调度算法 概要
先来先服务(First Come First Service,FCFS)调度算法 按照进程进入就绪队列的先后顺序选择可以占用处理器的进程。这是一种不可抢占方式的调度算法,优点是实现简单,缺点是后来的进程等待CPU的时间较长。它现今主要用作辅助调度法;例如结合在优先级调度算法中使用,当有两个最高优先级的进程时,则谁先来,谁就先被调度。
短作业(进程)优先调度算法SJ(P)F 是指对短作业或短进程优先调度的算法。它们可以分别用于作业调度和进程调度。短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度。
高优先权优先调度 为了照顾紧迫型作业,使之在进入系统后便获得优先处理,引入了最高优先权优先(FPF)调度算法。此算法常被用于批处理系统中,作为作业调度算法,也作为多种操作系统中的进程调度算法,还可用于实时系统中。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程。
高响应比优先调度算法 为了弥补短作业优先算法的不足,我们引入动态优先权,使作业的优先等级随着等待时间的增加而以速率a提高。 该优先权变化规律可描述为:优先权=(等待时间+要求服务时间)/要求服务时间;即 =(响应时间)/要求服务时间。优点是兼顾长短作业,缺点是计算响应比开销大,适用于批处理系统。
基于时间片的轮转调度算法 时间片轮转法一般用于进程调度,每次调度,把CPU分配队首进程,并令其执行一个时间片。 当执行的时间片用完时,由一个记时器发出一个时钟中断请求,该进程被停止,并被送往就绪队列末尾;依次循环。
多级反馈队列调度算法 将时间片轮转与优先级调度相结合,把进程按优先级分成不同的队列,先按优先级调度,优先级相同的,按时间片轮转。优点是兼顾长短作业,有较好的响应时间,可行性强,适用于各种作业环境。
  • 多级反馈队列调度算法实施过程
  1. 设置多个就绪队列,并为各个队列赋予不同的优先级。在优先权越高的队列中, 为每个进程所规定的执行时间片就越小。
  2. 当一个新进程进入内存后,首先放入第一队列的末尾,按FCFS原则排队等候调度。 如果他能在一个时间片中完成,便可撤离;如果未完成,就转入第二队列的末尾,在同样等待调度…… 如此下去,当一个长作业(进程)从第一队列依次将到第n队列(最后队列)后,便按第n队列时间片轮转运行。
  3. 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1到第(i-1)队列空时, 才会调度第i队列中的进程运行,并执行相应的时间片轮转。
  4. 如果处理机正在处理第i队列中某进程,又有新进程进入优先权较高的队列, 则此新队列抢占正在运行的处理机,并把正在运行的进程放在第i队列的队尾。
-------------本文结束感谢您的阅读-------------

本文标题:Linux_进程概念

文章作者:李煜哲

发布时间:2018年11月24日 - 22:11

最后更新:2018年11月24日 - 22:11

原始链接:http://yoursite.com/2018/11/24/Linux-进程概念/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者

觉得好的话就打赏一下吧