课本概念: 程序的一个执行实例,正在执行的程序等
内核观点: 担当分配系统资源(CPU时间,内存)的实体。
当我们打开可执行程序时,本质上就是把该程序加载到内存中。只有将其加载到内存中,CPU才能进行逐行的语句执行。当程序加载到内存时,我们可以称之为进程。
我们可以用ps aux查看当前系统中存在的进程
Linux进程控制模块是用结构体实现的
task_struct当中主要包含以下信息
操作系统是如何对进程进行管理的?
这里要牢记六字真言:先描述,再组织。 当进程出现时,操作系统立刻对其进行描述,再对描述的内容进行管理,将其储存在进程控制模块的数据结构中,俗称PCB(process control block)。
操作系统将每一个进程都进行描述,形成了一个个的进程控制块(PCB),并将这些PCB以双链表的形式组织起来。
进程的信息可以通过 /proc 系统文件夹查看
如上图系统文件中有很多数字,这些数字就是进程的PID,对应的文件中记录着该进程的各种信息。若想查看进程的信息打开该文件夹即可。
单独使用ps命令,会显示所有进程信息。
指令: ps aux
ps命令与grep命令搭配使用,即可只显示某一进程的信息。
指令: ps aux | head -1 && ps aux | grep myproc(进程) | grep -v grep
我们可以通过getpid和getppid即可分别获取进程的PID和PPID
当上述代码运行时,可循环打印PID,PPID
我们可以通过ps指令查看更全面的信息
观察下面代码
运行结果
这里使用fork函数创建子进程,父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
若是想让父子进程执行不同的功能,上文程序达不到要求,这里就要用到if分流
fork函数的返回值:
1、如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0。
2、如果子进程创建失败,则在父进程中返回 -1。
通过fork返回值的不同,可以让父子进程执行不同的功能
例
此时父子进程可执行不同的功能
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 *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*/ };
在Linux操作系统当中我们可以通过 ps aux 或 ps axj 命令查看进程的状态。
一个进程处于运行状态(running),并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
所有处于运行状态,即可被调度的进程,都被放到运行队列当中,当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行。
一个进程处于浅度睡眠状态(sleeping),意味着该进程正在等待某件事情的完成,处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉(这里的睡眠有时候也可叫做可中断睡眠(interruptible sleep))。
执行如下代码
在进程休眠的100秒钟我们可以看到进程处于浅度睡眠状态
而处于浅度睡眠状态的进程是可以被杀掉的,我们可以使用kill命令将该进程杀掉。
一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束。
例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。(磁盘休眠状态)
可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
例
注意:
可以通过kill -l指令查看当前系统所支持的信号集。
死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以在任务列表当中不易看到死亡状态(dead)。
我们来模拟一个僵尸进程
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { printf("I am running...\n"); pid_t id = fork(); if(id == 0){ //child 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){ //father while(1){ printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid()); sleep(1); } } else{ //fork error } return 0; }
我们可以通过循环指令不断查看进程状态来观察僵尸进程
Linux下循环执行某个指令方式
while :; do (指令内容); sleep 1; done
可以看出子进程退出后就变成了僵尸进程
父进程先退出,子进程就称之为“孤儿进程”,孤儿进程被1号init进程领养,由init进程回收。
模拟一个孤儿进程
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<unistd.h> 4 5 int main() 6 { 7 printf("I am running...\n"); 8 pid_t id = fork(); 9 if(id == 0){ //child 10 while(1){ 11 printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid()); 12 sleep(1); 13 } 14 } 15 else if(id > 0){ //father 16 int count=5; 17 while(count){ 18 printf("I am father...PID:%d, PPID:%d,count:%d\n", getpid(), getppid(),count); 19 sleep(1); 20 count--; 21 } 22 printf("father quit ... \n"); 23 exit(1); 24 25 } 26 else{ //fork error 27 } 28 return 0; 29 }
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
其中的几个重要信息如下:
注意: 在Linux操作系统当中,PRI(old)默认为80,即PRI = 80 + NI。
top命令就相当于Windows操作系统中的任务管理器,它能够动态实时的显示系统当中进程的资源占用情况。
使用top命令后按“r”键,会要求你输入待调整nice值的进程的PID。
输入进程PID并回车后,会要求你输入调整后的nice值。
输入nice值后按“q”即可退出
注意: 若是想将NI值调为负值,也就是将进程的优先级调高,需要使用sudo命令提升权限。
使用renice命令,后面跟上更改后的nice值和进程的PID即可。
注意: 若是想使用renice命令将NI值调为负值,也需要使用sudo命令提升权限。
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便有了优先级。
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
并行: 多个进程在多个CPU下分别同时进行运行,这称之为并行。
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
我们可以通过echo命令来查看环境变量
echo $NAME //NAME为待查看的环境变量名称
例:
查看环境变量PATH
为什么我们自己生成的可执行程序必须要在前面带上./才可以执行,而系统的指令可以直接执行?
当想运行一条可执行程序时,系统必须知道该程序的路径。系统提供的指令存放在指定路径下,而我们自己创建的可执行程序存放在当前路径下,所以需要在前面带上./来提示系统。
系统提供的指令存放路径如下
可以看到环境变量PATH当中有多条路径,这些路径由冒号隔开,当你使用ls命令时,系统就会查看环境变量PATH,然后默认从左到右依次在各个路径当中进行查找。
我们也可以查看任意一条系统指令存放的路径
有没有社什么办法让自己编译的可执行程序可以直接运行呢?
下面有两种方法
方式一:将可执行程序拷贝到环境变量PATH的某一路径下。
方式二:将可执行程序所在的目录导入到环境变量PATH当中。
任何一个用户在运行系统登录时都有自己的主工作目录(家目录),环境变量HOME当中即保存的该用户的主工作目录。
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。
main是有参数的,只是平时我们用不到
main函数有三个参数
int main(int argc, char *argv[], char *env[])
我们先讲前两个参数
输入如下代码
运行结果如下
main函数的第二个参数是一个字符指针数组,数组当中的第一个字符指针存储的是可执行程序的位置,其余字符指针存储的是所给的若干选项,最后一个字符指针为空,而main函数的第一个参数代表的就是字符指针数组当中的有效元素个数。
通过这两个参数可以让一条指令添加不同的符号执行不同的功能
如 ls -a 列出目录下的所有文件,包括以 . 开头的隐含文件。
而 ls -l 是列出文件的详细信息。
下面模拟实现指令+不同符号执行不同功能
#include <stdio.h> #include <string.h> int main(int argc, char *argv[], char* envp[]) { if(argc > 1) { if(strcmp(argv[1], "-a") == 0) { printf("you used -a option...\n"); } else if(strcmp(argv[1], "-b") == 0) { printf("you used -b option...\n"); } else { printf("you used unrecognizable option...\n"); } } else { printf("you did not use any option...\n"); } return 0; }
main函数的第三个参数接收的是环境变量表,我们可以通过main函数的第三个参数来获取系统的环境变量。
例
运行结果就是各个环境变量的值:
除了使用main函数的第三个参数来获取环境变量以外,我们还可以通过第三方变量environ来获取。
注意: libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern进行声明。
除了通过main函数的第三个参数和第三方变量environ来获取环境变量外,我们还可以通过系统调用getenv函数来获取环境变量。getenv函数可以根据所给环境变量名,在环境变量表当中进行搜索,并返回一个指向相应值的字符串指针。
例:用getenv函数获取环境变量PATH
运行结果如下
程序地址空间分布图:
观察下面代码及运行结果
#include <stdio.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){ //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取 g_val=100; printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val); }else{ //parent sleep(3); printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val); } sleep(1); return 0; }
从运行结果可看出,父子进程在同一块地址上取的值竟然不相同,这时为什么呢?
实际上,我们在语言层面上打印出来的地址都不是物理地址,而是虚拟地址。物理地址用户是看不到的,是由操作系统统一进行管理的。
注意: 虚拟地址和物理地址之间的转化由操作系统完成。
进程地址空间是内存中的一种内核数据结构,在Linux当中进程地址空间由结构体mm_struct实现。
进程地址空间将0x00000000到0xffffffff地址划分为各个区域,如代码区、堆区、栈区等。而结构体mm_struct记录了各个边界刻度。
在结构体mm_struct当中,各个边界刻度之间的每一个刻度都代表一个虚拟地址,这些虚拟地址通过页表映射与物理内存建立联系。由于虚拟地址是由0x00000000到0xffffffff线性增长的,所以虚拟地址又叫做线性地址。
每个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也会随之被创建。而操作系统可以通过进程的task_struct找到其mm_struct,因为task_struct当中有一个结构体指针存储的是mm_struct的地址。
例如,父进程有自己的task_struct和mm_struct,该父进程创建的子进程也有属于其自己的task_struct和mm_struct,父子进程的进程地址空间当中的各个虚拟地址分别通过页表映射到物理内存的某个位置,如下图:
而当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。
当子进程的值修改时,物理内存需要开一块空间储存新值,并改变虚拟地址通过页表映射的物理地址。
下面回答几个问题
进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。
子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间。
正常情况下是不会的,只有再特殊情况下才会,例如在进行进程替换的时候,则需要进行代码的写时拷贝。
(1)有了进程地址空间后,就不会有任何系统级别的越界问题存在了。例如进程1不会错误的访问到进程2的物理地址空间,因为你对某一地址空间进行操作之前需要先通过页表映射到物理内存,而页表只会映射属于你的物理内存。总的来说,虚拟地址和页表的配合使用,本质功能就是包含内存。
(2)有了进程地址空间后,每个进程都认为看得到都是相同的空间范围,包括进程地址空间的构成和内部区域的划分顺序等都是相同的,这样一来我们在编写程序的时候就只需关注虚拟地址,而无需关注数据在物理内存当中实际的存储位置。
(3)有了进程地址空间后,每个进程都认为自己在独占内存,这样能更好的完成进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),并能将进程调度与内存管理进行解耦或分离。