从用户角度看:进程就是当前正在运行中的程序
从操作系统角度:是程序运行的动态描述-pcb,结构体中包含程序运行的各种信息,实现操作系统对于运行中程序的管理。
PCB(process control block)也叫进程控制块,PCB其实是一个结构体对象,用来描述运行中的程序
的各种信息,Linux操作系统下的PCB是task_struct。
操作系统对进程的管理,总体来说六个字先描述,后组织。
当进程创建时,也就是我们的代码程序运行在操作系统上时,操作系统首先会用PCB对进程进行描述,之后对进程的管理其实就是对PCB的管理,也就是对PCB对象的管理。
操作系统对每一个进程都进行了管理,形成了一个个进程控制块(PCB对象),并将这些对象以链表的形式组织起来,这样一来,对进程的管理就成了对链表的管理。
在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 * const task_state_array[] = {
"R (running)", /* 0 */[重点]
"S (sleeping)", /* 1 */[重点]
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */[重点]
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */[重点]
};
一个进程处于运行状态(running),并不一定程序就正在运行,有可能是正在running的进程,也有可能是可以执行,但是尚未被CPU调度的ready状态。这些进程的PCB被组织在可执行队列中,一旦拿到时间片就能立即运行。
可以中断的阻塞状态,这些进程因为等待某些事件的发生而被挂起,等到资源到位或者收到信号,可以立即被唤醒进入运行状态的状态。
这个状态的进程处于阻塞状态且不能被唤醒,一般进程在进行磁盘IO时处于这个状态,这时的进程是不能被唤醒运行的,要等到IO结束之后才会被唤醒。
这时的状态处于停止状态(暂停状态),可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
当一个进程的子进程先于父进程退出,并且父进程没有调用wait或waitpid回收子进程。此时子进程即处于僵尸状态
如果一个进程处于僵尸状态,也就是这个进程先于父进程退出,而且这个进程的父进程没有对这个进程进行处理,那么这个进程就是僵尸进程。
为什么会有僵尸状态?
进程在退出的时候会有一个退出码和一些统计信息,这些信息会被保存在PCB里面,而父进程在子进程退出的时候要获取这些信息,并对子进程进行处理,所以在子进程退出时,父进程不处理,那这个进程就会变为僵尸进程。
僵尸进程的危害
如果产生了僵尸进程,并且不处理,就会产生内存泄漏。因为僵尸状态的进程会保存退出码和一些信息,PCB不会完全释放,这些PCB还是要被操作系统管理,会占用内存资源和进程数量。
僵尸进程的避免和处理
如果一个进程的父进程先于子进程退出,那么这个子进程就成为孤儿进程
特性:
父进程退出后,子进程会被1号进程领养,那么这个进程的父进程就会变为1号进程。孤儿进程是运行在后台的。
特殊的孤儿进程,也叫精灵进程,在孤儿进程的基础上脱离与终端之间的关系。
如果我们想将一个程序稳定运行在后台,不受任何影响,这个时候将这个进程转换为守护进程。
环境变量是指操作系统用来指定操作系统运行环境的一些参数
**特性:**环境变量具有进程之间的传递性
**作用:**用于进程之间的一些数据传递
- PATH : 指定命令的搜索路径
- HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL : 当前Shell,它的值通常是/bin/bash
#include
using namespace std;
//第一个参数是命令数量,第二个是命令表,第三个是环境表
int main(int argc, char* argv[], char* env[]) {
for (int i = 0; env[i]; ++i) {
cout << env[i] << endl;
}
return 0;
}
#include
int main(int argc, char* argv[])
{
extern char** environ;
int i = 0;
for (; environ[i]; i++) {
printf("%s\n", environ[i]);
}
return 0;
}
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
#include
#include
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
我们以前知道,程序的不同变量和资源存放在不同的内存区域,每个内存区域都有自己的专有的存储对象,如下图所示就是程序的地址空间分布。
地址空间的作用:仅仅限定了一块内存空间,属于当前进程的一段活动范围,并不代表这个进程把所有资源都占有了
看一段代码
#include
#include
int val = 888;
int main() {
pid_t ret = fork();
if (ret > 0) {
//parent
while (1) {
sleep(1);
printf("%d %p\n", val, &val);
}
}
else if (ret == 0) {
//child
while (1) {
sleep(1);
val = 999;
printf("%d %p\n", val, &val);
}
}
else {
perror("fork error!!!");
return 1;
}
return 0;
}
我们给出一个全局变量,赋值为888,并且在子进程中修改变量的值,然后让父进程和子进程循环打印,并且打印出变量的地址
我们发现,子进程和父进程打印出的变量,值不一样!但是变量的地址却是一样的。
当数据没有改变时,子进程继承了父进程的大部分信息,父子进程共用同一块地址空间,但是当数据进行修改时,由于进程之间的数据独立,子进程就会重新开一块空间构建页表,映射属于自己的虚拟地址空间。
在Linux系统中,虚拟地址空间实际上是一个结构体mm_struct ,task_struct中有这个结构体的指针,每个进程的地址空间都是通过这个结构体进行管理
申请空间的本质是:向内存索要空间,得到物理地址。然后在特定区域申请没有被使用的虚拟地址,建立映射关系,返回虚拟地址即可。
地址空间的本质实际上是描述进程所占用物理内存的一个数据结构mm_struct,存在操作系统的内核中,task_struct的结构体内有指针指向mm_struct用来管理地址空间。
每个进程都有一个进程地址空间,所以系统内有多个地址空间,要管理这些地址空间,就得先把这些地址空间描述起来。
地址空间本质就是一个数据结构,在Linux系统中,叫做mm_struct,它包含了一些区域信息,能够实现区域划分。
在内核源码(/usr/src/kernels/3.10.0-1160.31.1.el7.x86_64/include/linux/mm_types.h)中,有这样一些信息,实现区域的划分。
struct mm_struct {
......
......
......
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
......
......
}
进程的PCB即task_struct里面存着指向mm_struct的指针,mm_struct的对象每个进程独有一份,PCB通过mm_struct管理空间,把内存组织起来。
地址空间上所呈现暴露给上层的所有的地址都叫做虚拟地址,而实际进程访问时,要通过页表映射转换到物理内存,然后拿到对应的代码和数据。