进程概念
进程Process是指计算机中已运行的程序,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
- 在早期面向进程设计的计算机结构中,进程是程序的基本执行实体。
- 在当代面向线程设计的计算机结构中,进程是线程的容器。
- 进程是程序真正运行的实例,若干进程可能与同一个程序相关,且每个进程皆可以同步或异步的方式独立运行。
狭义定义:进程是正在运行的程序的实例。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
担当分配系统资源(CPU时间,内存)的实体。
进程与程序的区别
- 程序是永存的;进程是暂时的,是程序在数据集上的一次执行,有创建有撤销,存在是暂时的;
- 程序是静态的观念,进程是动态的观念;
- 进程具有并发性,而程序没有;
- 进程是竞争计算机资源的基本单位,程序不是。
- 进程和程序不是一一对应的:
- 一个程序可对应多个进程即多个进程可执行同一程序;
- 一个进程可以执行一个或几个程序
并发与并行的区别
- 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。
- 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在,并发能够在单处理器系统中存在是因为并发是并行的假象,并行要求程序能够同时执行多个操作,而并发只是要求程序假装同时执行多个操作。
描述进程—PCB
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct 。
- 在Linux中描述进程的结构体叫做task_struct。
- task_struct是Linux内核的⼀一种数据结构,它会被装载到RAM(内存)⾥里并且包含着进程的信息。
task_struct内容分类
在进程执行时,任意给定一个时间,进程都可以唯一的被表征为以下元素。
标示符: 描述本进程的唯一标示符,⽤用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据
I/O状态信息: 包括显⽰示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
查看进程
进程的信息可以通过/proc
系统文件夹来查看
- 要获取PID为多少的进程信息,需要在命令行中输入
ls /proc/PID
- 大多数进程信息可以使用top和ps这些用户级工具来获取
为什么我把进程删除之后,还能查看到?
因为进程已经运行起来了,寄存器中有了所以可以查看到。
进程的特征
进程是由多程序的并发执行而引出的,它和程序是两个截然不同的概念。进程的基本特征是对比单个程序的顺序执行提出的,也是对进程管理提出的基本要求。
- 动态性:进程是程序的一次执行,它有着创建、活动、暂停、终止等过程,具有一定的生命周期,是动态地产生、变化和消亡的。动态性是进程最基本的特征。
- 并发性:指多个进程实体,同存于内存中,能在一段时间内同时运行,并发性是进程的重要特征,同时也是操作系统的重要特征。引入进程的目的就是为了使程序能与其他进程的程序并发执行,以提高资源利用率。
- 独立性:指进程实体是一个能独立运行、独立获得资源和独立接受调度的基本单位。凡未建立PCB的程序都不能作为一个独立的单位参与运行。
- 异步性:由于进程的相互制约,使进程具有执行的间断性,即进程按各自独立的、 不可预知的速度向前推进。异步性会导致执行结果的不可再现性,为此,在操作系统中必须配置相应的进程同步机制。
- 结构性:每个进程都配置一个PCB对其进行描述。从结构上看,进程实体是由程序段、数据段和进程控制段三部分组成的。
进程的状态和转换
- 运行状态:进程正在处理机上运行。在单处理机环境下,每一时刻最多只有一个进程处于运行状态。
- 就绪状态:进程已处于准备运行的状态,进程获得除处理机之外的一切所需资源,一旦得到处理机即可运行。
- 阻塞状态:又称等待状态:进程正在等待某一事件而暂停运行,如等待某资源为可用(不包括处理机)或等待输入/输出完成。即使处理机空闲,该进程也不能运行。
- 创建状态:进程正在被创建,尚未转到就绪状态。创建进程通常需要多个步骤:首先申请一个空白的PCB,并向PCB中填写一些控制和管理进程的信息;然后由系统为该进程分 配运行时所必需的资源;最后把该进程转入到就绪状态。
- 结束状态:进程正从系统中消失,这可能是进程正常结束或其他原因中断退出运行。当进程需要结束运行时,系统首先必须置该进程为结束状态,然后再进一步处理资源释放和 回收等工作。
- 就绪状态 -> 运行状态:处于就绪状态的进程被调度后,获得处理机资源(分派处理机时间片),于是进程由就绪状态转换为运行状态。
- 运行状态 -> 就绪状态:处于运行状态的进程在时间片用完后,不得不让出处理机,从而进程由运行状态转换为就绪状态。此外,在可剥夺的操作系统中,当有更高优先级的进程就 、 绪时,调度程度将正执行的进程转换为就绪状态,让更高优先级的进程执行。
- 运行状态 -> 阻塞状态:当进程请求某一资源(如外设)的使用和分配或等待某一事件的发生(如I/O操作的完成)时,它就从运行状态转换为阻塞状态。进程以系统调用的形式请求操作系统提供服务,这是一种特殊的、由运行用户态程序调用操作系统内核过程的形式。
- 阻塞状态 -> 就绪状态:当进程等待的事件到来时 ,如I/O操作结束或中断结束时,中断处理程序必须把相应进程的状态由阻塞状态转换为就绪状态。
通过系统调用获取进程表示符
- 进程id(PID)
- 父进程id(PPID)
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>int main()
{printf("pid:%d\n",getpid());printf("ppid:%d\n",getppid());return 0;
}
通过系统调用创建进程
以父进程为模板为子进程创建PCB,刚创建的时候子进程和父进程共同用代码和数据,父子进程都要执行fork函数之后的代码。
- fork函数的作用就是创建子进程的。
- fork函数有两个返回值
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>int main()
{printf("hello proc:%d\n",getpid());int ret=fork();printf("hello proc:%d!,ret:%d\n",getpid(),ret);sleep(1);return 0;
}
- 子进程可以看到fork之前的代码?
可以看到。
- 那为什么子进程不执行之前的代码?
因为有寄存器的存在pc/eip执行fork完毕之后,eip指向fork之后的代码。
- fork干了什么事情?
创建了一个进程。
- 为什么fork函数会返回两个不同的值?
因为有两个执行流去执行相同的代码,而fork中肯定会有return语句,那么它也要被执行两次所以就产生了两个不同的返回值。
- 为什么父进程返回子进程pid,子返回0呢?
父:子=1:n ,为了区分子进程。
- fork之后,父进程和子进程谁先运行?
由IO来决定
- 如何理解同一个变量有不同给的值?
但子进程要改变的时候就会发生写时拷贝。
fork之后通常要用if进行分流
#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){printf("I am child:%d!,ret:%d\n",getpid(),ret);}else{printf("I am father:%d!,ret:%d\n",getpid(),ret);}sleep(1);return 0;
}
进程=可执行程序+内核数据结构(PCB)
进程状态
孤儿进程
父进程先于子进程退出,子进程会被systemd进程收养,子进程会变成后台进程
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>int main()
{pid_t id=fork();if(id<0){perror("fork");return 1;}else if(id==0){printf("child pid %d\n",getpid());sleep(20);}else {printf("parent pid %d\n",getpid());sleep(3);exit(0);}return 0;
}
僵尸进程
子进程先退出,父进程没有及时回收子进程的资源(PCB结构体),此时就成为了僵尸进程
注意:父进程不退出,子进程会一直保持僵死状态,直到父进程退出,被systemd回收
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>int main()
{pid_t id=fork();if(id<0){perror("fork");return 1;}else if(id>0){printf("parent[%d] is sleeping...\n",getpid());sleep(30);}else{printf("child[%d] is begin Z...\n",getpid());sleep(10);exit(EXIT_SUCCESS);}return 0;
}
如何避免僵尸进程,子进程退出时父进程及时回收子进程的资源。
- R运行状态(running):并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
当操作系统把CPU资源分配给进程时,进程进入运行状态。在运行状态下,进程会执行它所分配的任务。当进程的时间片用完后,进程就会被挂起,等待下一次CPU调度。
- S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
睡眠状态是指:当进程被调用而等待某个事件的发生时,该进程就会进入睡眠状态。通俗来讲,当我们要完成某种任务的时候,任务条件不具备,需要进程进行某种等待。
进程等待时,都是在等待CPU资源吗?答案是:并不是!
进程处于睡眠状态时,等待的并不是CPU资源。在这个状态下,进程会放弃CPU资源,同时等待某个事件的发生。例如,一个进程在等待磁盘IO操作完成时,就会进入睡眠状态。当磁盘IO操作完成时,内核会将该进程唤醒,并将其转移到就绪状态,这样该进程就可以重新开始执行。
睡眠状态被认为是一种阻塞状态,因为该进程在等待发生的事件情况下不能真正地执行,所以CPU的资源也就会被阻塞。只有当该事件发生,进程才能被唤醒并继续执行。
- D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
比如有一个进程需要把数据写入磁盘,且数据量很大。而写入磁盘的过程相对来说并不是很快,进程需要等待一段时间。等待的过程中,同时也占用了CPU的资源,但CPU会把时间片分给其他进程。该进程又需要磁盘读取数据完成后的返回信息(返回信息是指磁盘是否读取数据完成),所以该进程不可被操作系统终止。
- T停止状态(stopped):可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- t停止状态 (tracing stop):t状态是由于进程被 GDB 或其他调试器停止。在这个状态下,进程被挂起,等待被调试器唤醒。在此状态下,进程无法从内核的角度运行,但是仍然占用系统资源(如内存和CPU),因此这种状态一般不会持续太久,很快就会被唤醒。
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
为什么进程状态后面又一个+号?
Linux中运行的程序可以在前台(foreground)或者后台(background)运行。前台程序是在当前终端会话中运行的程序,而后台程序则是在系统内部运行的,没有与用户终端会话相关联。上述带 ‘+’ 号的就是在前台运行的程序,不带 ‘+’ 号的就是在后台运行的程序。
程序在后台运行时用户可以继续在终端上执行其他命令,而程序则在后台默默运行。注意:此时的ctrl + c 并不能终止后台进程,我们通过信号对后台进程进程终止。
$ jobs #查看后台运行的程序
$ fg %290 #将后台中的程序290转为前台运行
进程优先级
CPU分配资源的先后顺序,就是指进程的优先权。
优先权高的进程有优先执行权,在配置进程优先权对多任务环境的Linux很有用,并且还可以把重要的进程运行到指定的CPU上,把不重要的进程安排到某个CPU,能大大提高系统的性能。
查看系统进程
可以用静态的ps或者是动态的top,还能以pstree来查看程序树之间的关系。
UID:执行者的身份。
PID:本进程代号。
PPID:父进程代号。
PRI:本进程被执行的优先级,其值越小越早被执行。
NI:本进程的nice值。
为什么存在进程的优先级?
本质就是因为CPU的资源不足造成的。
PRI=PRI(old)+NI
PRI都是从80开始的,和上一次变化没有任何关系。
为什么要把优先级限定在一定的范围内?
OS调度的时候要较为公平,较为均衡地调度每一个进程。
如果优先级范围过大,容易导致优先级低的进程长时间得不到CPU资源,造成进程饥饿问题。
NI
通俗简单一点来讲呢,就是进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
用top命令更改已存在进程nice
首先的话我们执行[top]指令,进入到Linux下类似于【任务管理器】的界面
接下去按下R/r之后,就会跳出来下面这句话,这里的renice指的就是要重新修改进程,此时 shell 正在等待我们输入进程的PID值,那我们就可以输入上方的243950
然后呢,我们看到 shell 又在等待我们输入需要更改的nice值
环境变量
环境变量一般指的是在操作系统中用于指定操作系统运行环境的一些参数。
- 在链接C/C++程序的代码时,会用到动静态库,但是动静态库我们并没有显示的去指定过让系统去那个路径下寻找它,但是每次都可以链接成功,这是因为有了环境变量的帮助。
- 环境变量还会有一些特殊的用途,比如可以用作身份验证。
环境变量分类
- 按照生命周期来分,Linux环境变量可以分为两类:
-
- 永久的:需要用户修改相关的配置文件,变量永久生效
- 临时的:用户利用export命令,在当前终端下声明环境变量,关闭Shell终端失效。
- 按照作用域来分,Linux环境变量可以分为:
-
- 系统环境变量:系统环境变量对该系统中所有用户都有效。
- 用户环境变量:顾名思义,这种类型的环境变量只对特定的用户有效。
常见环境变量
PATH:决定了 shell 将到哪些目录中寻找命令或程序
HOME:当前用户主目录(就是用户登录linux系统中时,默认的目录
USER:当前用户的用户名。
HISTSIZE:历史记录数
LOGNAME:当前用户的登录名
HOSTNAME:指主机的名称
SHELL:当前用户 Shell 类型,它的通常值是/bin/bash
LANGUGE:语言相关的环境变量,多语言可以修改此环境变量
MAIL:当前用户的邮件存放目录
PS1:基本提示符,对于 root 用户是 #,对于普通用户是 $
查看环境变量方法
echo $PATH
添加环境变量
PATH=$PATH:(需要添加的环境变量)
相关环境变量的命令
- echo:显示某个环境变量值
- export:设置一个新的环境变量
- env:显示所有环境变量
- unset:清除环境变量
- set:显示本地定义的shell变量和环境变量
通过代码获取环境变量
int main(int argc,char *argv[],char *env)
{int i=0;for(;env[i];i++){printf("%s\n",env[i]);}return 0;
}
程序地址空间
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>int g_val = 0;
int main()
{pid_t id = fork();if (id < 0){perror("fork");return 0;}else if (id == 0){ printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);}else{ printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);}sleep(1);return 0;
}
我们发现,输出出来的变量值和地址是一模一样的,很好理解呀,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>int g_val = 0;
int main()
{pid_t id = fork();if (id < 0){perror("fork");return 0;}else if (id == 0){ g_val=100;printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);}else{ printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);}sleep(1);return 0;
}
我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量。
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做 虚拟地址
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理 OS必须负责将 虚拟地址 转化成 物理地址 。
进程调度队列
如果有多个CPU就要考虑进程个数的负载均衡问题。
普通优先级:100~139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
实时优先级:0~99(不关心)
活动队列
- 时间片还没有结束的所有进程都按照优先级放在该队列
- nr_active: 总共有多少个运行状态的进程
- queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下 标就是优先级!
- 从该结构中,选择一个最合适的进程,过程是怎么的呢?
- 1. 从0下表开始遍历queue[140]
- 2. 找到第一个非空队列,该队列必定为优先级最高的队列
- 3. 拿到选中队列的第一个进程,开始运行,调度完成!
- 4. 遍历queue[140]时间复杂度是常数!但还是太低效了!
- bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个
- 比特位表示队列是否为空,这样,便可以大大提高查找效率。
过期队列
- 过期队列和活动队列结构一模一样
- 过期队列上放置的进程,都是时间片耗尽的进程
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
active指针和expired指针
- active指针永远指向活动队列
- expired指针永远指向过期队列
- 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。
- 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!