目录
一、进程概念
二、描述进程PCB
三、查看进程
3.1 通过系统目录查看
3.2 通过ps命令查看
四、进程状态
运行状态R
睡眠状态S
磁盘休眠状态D
暂停状态T
僵尸状态Z
死亡状态X
五、僵尸进程与孤儿进程
5.1 僵尸进程
5.1.1 僵尸进程的概念
5.1.2 僵尸进程的危害
5.2 孤儿进程
六、进程地址空间
6.1 进程地址空间的验证
6.2 感知进程地址空间
6.3 详细认知
七、进程优先级
7.1 优先级概念
7.2 优先级存在的原因
7.3 PRI与NI
7.4 修改进程nice值
7.4.1 top命令
7.4.2 renice命令
八、环境变量
8.1 概念
8.2 常见环境变量
8.3 环境变量相关命令
8.4 环境变量的组织方式
8.5 获取环境变量的方法
8.5.1 main函数参数
8.5.2 第三方变量environ
8.5.3 getenv函数
一、进程概念
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体
当代码进行编译链接等操作后就会生成一个可执行程序,这个可执行程序本质上也是一个文件,存放在磁盘上。当使这个可执行程序运行起来,本质上是将这个程序加载到内存当中了,因为只有加载到内存后,CPU才能对其进行逐行的语句执行,而一旦将这个程序加载到内存后,我们就不应该将这个程序再叫做程序了,严格意义上将应该将其称之为进程。
并且进程与程序并不一定是对应的,一个程序可以同时运行多次,也就有了多个进程。
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
二、描述进程PCB
我们的电脑上存在着大量的进程,这时就需要操作系统来进行管理。如何管理呢?先描述,再组织
操作系统将每一个进程都进行描述,形成了一个个的进程控制块(PCB,本质上是一个结构体),并将这些PCB以双向链表的形式组织起来。
PCB实际上是对进程控制块的统称,Linux当中的进程控制块为 task_struct,task_struct当中主要包含以下信息:
标示符:描述本进程的唯一标示符,用来区别其他进程。
状态:任务状态,退出代码,退出信号等。
优先级:相对于其他进程的优先级。
程序计数器(pc):程序中即将被执行的下一条指令的地址。
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
上下文数据:进程执行时处理器的寄存器中的数据。
I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息:可能包括处理器时间总和,使用的时钟总和,时间限制,记账号等。
其他信息: ……
三、查看进程
3.1 通过系统目录查看
在根目录下有一个名为proc的系统目录,该目录中包含大量进程信息。其中有些子目录的目录名为数字,这些数字其实是某一进程的PID,对应文件夹当中记录着对应进程的各种信息。若想查看PID为1的进程的进程信息,则查看名字为1的文件夹即可。
3.2 通过ps命令查看
ps命令的具体使用可以使用man 1 ps命令查看文档
在Linux操作系统下使用ps -l命令会出现下列这种情况。
- UID:代表执行者的身份。
- PID:代表这个进程的代号。
- PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号。
- PRI:代表这个进程可被执行的优先级,其值越小越早被执行。
- NI:代表这个进程的nice值。
四、进程状态
Linux操作系统的源代码当中对于进程状态有如下定义:
static const char *task_state_array[] = {
"R (running)", /* 0*/
"S (sleeping)", /* 1*/
"D (disk sleep)", /* 2*/
"T (stopped)", /* 4*/
"T (tracing stop)", /* 8*/
"Z (zombie)", /* 16*/
"X (dead)" /* 32*/
};
运行状态R
所有处于运行状态的进程(即可被调度的进程),都被放到运行队列当中。当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行。一个进程处于运行状态(running),并不意味着进程一定处于运行当中。运行状态表明一个进程要么在运行中,要么在运行队列里。即可以同时存在多个R状态的进程
睡眠状态S
意味着进程在等待事件完成(该睡眠状态也可称为可中断睡眠)
譬如当进程循环向屏幕输出时,由于CPU的处理速度极快,但显示器的速度较慢,导致进程需等待显示器这个资源(CPU此时会处理别的进程)。此时该进程会在运行状态和睡眠状态不断切换,但由于CPU的高速导致我们观测时大概率会看见睡眠状态。
#include <stdio.h>
int main()
{
while(1){
printf("handsome boy!\n");
}
return 0;
}
显示状态时有个+号表示该进程是前台进程,若没有则是后台进程。
处于该睡眠状态下的进程是可以被杀死的,譬如使用kill命令发送信号
磁盘休眠状态D
一个进程处于磁盘休眠状态,表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动苏醒才可以杀死 。也可称为不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束。
譬如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不可被杀掉的。因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。
使用dd命令可以模拟磁盘休眠状态
暂停状态T
在Linux当中,我们可以通过发送SIGSTOP信号使进程进入暂停状态,发送SIGCONT信号可以让处于暂停状态的进程继续运行。
僵尸状态Z
当一个进程将要退出的时候,在系统层面,该进程曾经申请的资源并不是立即被释放,而是要暂时存储一段时间,以供操作系统或是其父进程进行读取,如果退出信息一直未被读取,则相关数据是不会被释放掉的,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。(进程的退出信息存储在该进程的task_struct中)
僵尸状态的存在是必要的,因为进程被创建的目的就是完成某项任务,那么当任务完成的时候,调用方是应该知道任务的完成情况的,所以必须存在僵尸状态,使得调用方得知任务的完成情况,以便进行相应的后续操作。
死亡状态X
死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以几乎不会在任务列表当中看到死亡状态。
五、僵尸进程与孤儿进程
5.1 僵尸进程
5.1.1 僵尸进程的概念
一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。而处于僵尸状态的进程,就是僵尸进程。
如下代码,fork函数创建的子进程在打印5次信息后会退出,而父进程会一直打印信息。即子进程退出了,父进程还在运行,但父进程没有读取子进程的退出信息,那么子进程就进入了僵尸状态。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 5;
while(count){
printf("I am child...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(1);
count--;
}
printf("child quit...\n");
exit(1);
}
else if(id > 0)
{
while(1){
printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
}
else{
exit(-1);
}
return 0;
}
5.1.2 僵尸进程的危害
- 若父进程一直不读取进程的退出信息,那么子进程将一直处于僵尸状态。
- 僵尸进程的退出信息被保存在task_struct中,若僵尸状态一直不退出,PCB就需一直维护。
- 若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费。
- 僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,僵尸进程会导致内存泄漏。
5.2 孤儿进程
若父进程先退出,那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为孤儿进程。若是一直不处理孤儿进程的退出信息,那么孤儿进程就会一直占用资源,此时就会造成内存泄漏。因此,当出现孤儿进程的时候,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由int进程进行处理回收。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0){ //child
int count = 5;
while(1){
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid(), count);
sleep(1);
}
}
else if(id > 0){ //father
int count = 5;
while(count){
printf("I am father...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(1);
count--;
}
printf("father quit...\n");
exit(0);
}
else{ //fork error
exit(-1);
}
return 0;
}
由于孤儿进程会被init1号进程领养,其并不会造成危害。
六、进程地址空间
6.1 进程地址空间的验证
通过如下代码可以验证进程地址空间与上图一致
#include <stdio.h>
#include <stdlib.h>
int un_val;
int init_val = 100;
int main(int argc,char* argv[],char* env[])
{
int i = 0;
int count = 0;
while(env[i] != NULL && count < 5){
printf("环境变量地址: %p\n",env[i]);
++count;
}
for(int i = 0;i < argc; ++i){
printf("命令行参数地址: %p\n",argv[i]);
}
char* p1 = (char*)malloc(10);
char* p2 = (char*)malloc(10);
char* p3 = (char*)malloc(10);
printf("栈区地址: %p\n",&p3);
printf("栈区地址: %p\n",&p2);
printf("栈区地址: %p\n",&p1);
printf("堆区地址: %p\n",p3);
printf("堆区地址: %p\n",p2);
printf("堆区地址: %p\n",p1);
printf("未初始化数据区: %p\n",&un_val);
printf("初始化数据区: %p\n",&init_val);
printf("代码区: %p\n",main);
return 0;
}
栈区向低地址增长,堆区向高地址增长。
6.2 感知进程地址空间
通过下面一份代码我们可以发现一个问题
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int val = 100;
int main()
{
pid_t id = fork();
if(id < 0)//err
{
exit(-1);
}
else if(id > 0)//father
{
sleep(3);
printf("PID:%d PPID:%d val:%d &val:%p\n",getpid(),getppid(),val,&val);
}
else//id == 0
{
val = 200;
printf("PID:%d PPID:%d val:%d &val:%p\n",getpid(),getppid(),val,&val);
}
return 0;
}
代码当中用fork函数创建了一个子进程,其中让子进程相将全局变量val该从100改为200后打印,而父进程先休眠3秒钟,然后再打印全局变量的值。按道理来说子进程打印的全局变量的值为200,而父进程是在子进程将全局变量改后再打印的全局变量,那么也应该是200。但是结果并不是,而且两个进程中val变量的地址是一样的,但是为什么打印出的结果不一致呢?
若我们是在同一个物理地址处获取的数据,那一定是相同的,而现在在同一个地址处获取到的值却不同,这只能说明我们打印出来的地址绝对不是物理地址。
实际上,我们在语言层面上打印出来的地址都不是物理地址,而是虚拟地址。物理地址用户一概是看不到的,是由操作系统统一进行管理的。所以就算父子进程当中打印出来的全局变量的地址(虚拟地址)相同,但是两个进程当中全局变量的值却是不同的。
6.3 详细认知
进程地址空间地址大小由0x00000000到0xffffffff,且被划分为各个区域,例如代码区、堆区、栈区等。而在结构体mm_struct当中,便记录了各个区域的边界地址。由于虚拟地址是由0x00000000到0xffffffff线性增长的,所以虚拟地址又叫做线性地址。
堆向上增长以及栈向下增长实际就是改变mm_struct当中堆和栈的边界地址。
我们生成的可执行程序实际上就被分为了各个区域(例如初始化区、未初始化区等),并且采用与Linux内核中一样的编址方式。当该可执行程序运行起来时,操作系统则将对应的数据加载到对应内存当中即可,大大提高了操作系统的工作效率。而进行可执行程序的"分区"操作的实际上就是编译器,所以说代码的优化级别实际上是编译器说了算。
每个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也会随之被创建。而操作系统可以通过进程的task_struct找到其mm_struct,因为task_struct当中有一个结构体指针存储的是mm_struct的地址。
而当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。体现了进程之间的独立性,这种在需要进行数据修改时才进行拷贝的技术被称为写时拷贝技术。
为什么不在创建子进程时完成数据的拷贝呢?
子进程大概率不会使用父进程中所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝。在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间
代码会不会进行写时拷贝?
大多数的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候就需要进行代码的写时拷贝。
为什么要有进程地址空间?
- 进程地址空间和页表是OS创建和管理的,凡是非法的访问或映射都会被操作系统终止,起到了保护物理内存中的所有合法数据(各个进程以及内核的相关有效数据),不会存在任何系统级别的越界问题了。
- 有了进程地址空间后,每个进程看到的都是相同的空间范围,包括进程地址空间的构成和内部区域的划分顺序等都是相同的,这样一来我们在编写程序的时候就只需关注虚拟地址,而无需关注数据在物理内存当中实际的存储位置。可以让进程以一种统一的视角看待内存,方便以统一的方式来编译和加载所有的可执行程序,简化进程本身的设计与实现。
- 有了进程地址空间后,每个进程都认为自己在独占内存,这样能更好的完成进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),并能将进程调度与内存管理进行解耦。
进程如何创建?
一个进程的创建伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)和页表的创建
七、进程优先级
7.1 优先级概念
优先级实际上就是获取某种资源的先后顺序,而进程优先级实际上就是进程获取CPU资源分配的先后顺序,就是指进程的优先权(priority),优先权高的进程有优先执行的权力。
7.2 优先级存在的原因
优先级存在的主要原因就是资源是有限的,而存在进程优先级的主要原因就是CPU资源是有限的,一个CPU一次只能跑一个进程,而进程是可以有多个的,所以需要存在进程优先级,来确定进程获取CPU资源的先后顺序。
7.3 PRI与NI
- PRI代表进程的优先级,即进程被CPU执行的先后顺序,该值越小进程的优先级别越高。
- NI代表的是nice值,其表示进程可被执行的优先级的修正数值。
- PRI值越小越快被执行,当加入nice值后,将会使得PRI变为:PRI(new) = PRI(old) + NI。
- 若NI值为负值,那么该进程的PRI将变小,即其优先级会变高。
- 调整进程优先级,在Linux下,就是调整进程的nice值。
- NI的取值范围是-20至19,一共40个级别。
注意: 在Linux操作系统当中,PRI(old)默认为80,即PRI = 80 + NI。
7.4 修改进程nice值
7.4.1 top命令
top命令就相当于Windows操作系统中的任务管理器,它能够动态实监测系统中进程资源占用情况
使用top命令后按“r”键,会要求你输入待调整nice值的进程的PID;输入进程PID并回车后,要求输入调整后的nice值。若想退出输入q即可。
7.4.2 renice命令
renice + 更改的nice值 + PID
若想使用renice命令将NI值调为负值,需要root权限
八、环境变量
8.1 概念
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。如编写的C/C++代码,在各个目标文件进行链接的时候,从来不知道所链接的动静态库在哪里,但依然可以链接成功生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,并且在系统当中通常具有全局特性。
8.2 常见环境变量
- PATH: 指定命令的搜索路径(系统命令本质也是可执行程序,但启动时不需要指定路径)
- HOME: 指定用户的主工作目录(即用户登录到Linux系统中的默认所处目录)
- SHELL: 当前Shell,它的值通常是/bin/bash
8.3 环境变量相关命令
echo:显示某个环境变量的值
export:设置一个新的环境变量
env: 显示所有环境变量
set:显示本地定义的shell变量和环境变量
unset:清除环境变量
8.4 环境变量的组织方式
在Linux系统当中,环境变量的组织方式如下:
每个程序都会收到一张环境变量表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串,最后一个字符指针为空。
8.5 获取环境变量的方法
8.5.1 main函数参数
main函数其实有三个形参,只是平时不常使用所以没有写出来。
int main(int argc, char* argv[],char* env[])
{ …… }
main函数的第二个参数是一个字符指针数组,该数组当中的第一个字符指针存储的是可执行程序的字符串,其余字符指针存储的是所给的若干选项的字符串,最后一个字符指针为空,而main函数的第一个参数代表的就是字符指针数组当中的有效元素个数。main函数的第三个参数接收的实际上就是环境变量表,我们可以通过main函数的第三个参数来获取系统的环境变量。
#include <stdio.h>
int main(int argc, char* argv[], char* env[])
{
for(int i = 0; env[i] != NULL; ++i){
printf("%s\n",env[i]);
}
return 0;
}
8.5.2 第三方变量environ
c语言给我们提供了一个全局变量environ,可以利用其访问环境表
#include <stdio.h>
int main()
{
extern char** environ;
for(int i = 0;environ[i] != NULL; ++i){
printf("%s\n",environ[i]);
}
return 0;
}
8.5.3 getenv函数
可以通过系统调用getenv函数来获取环境变量。getenv函数可以根据所给环境变量名,在环境变量表当中进行搜索,并返回一个指向相应值的字符串指针。
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n",getenv("PATH"));
return 0;
}