进程和线程
一、进程
- 我们的操作系统中运行着各种各样的程序,为了管理这些程序的运行,操作系统提出了进程。
- 为了使程序看起来是同一时间运行的,操作系统进一步提出了上下文切换的概念。
1.1、进程的状态
- 进程存在五个状态。
- 分别为:新生状态,预备状态,运行状态,阻塞状态,终止状态。
- 新生状态是指:一个进程刚被创建出来,还没有完成初始化,这里初始化的意思应该是给进程创建相应的数据结构等一系列操作。新生状态经过初始化之后,就进入了预备状态。
- 预备状态:表示该程序可以被调度执行,但是还没有被调度器选择。一旦被调度器选择之后,就进入了运行状态。
- 运行状态:该状态表示进程正在cpu上运行,当一个进程执行一段时间之后,调度器可以选择中断它的执行,并将其重新放回调度队列中,它就会重新变成预备状态。如果运行结束,它会迁移至终止状态。如果这个进程需要某些外部事件,它可以放弃cpu,并迁移至阻塞状态。
- 阻塞状态:该状态表示,进程需要某些外部事件的触发,比如按下空格键,外部事件触发之后,会迁移至预备状态。
- 终止状态:进程运行结束。
进程的内存空间布局
- 红色的是内核区域
- 蓝色的是非内核区域。
- 用户栈:存储进程需要使用的各种临时变量
- 代码库:进程执行时需要依赖的代码库,比如libc
- 用户堆:进程动态分配的内存,一般由程序员创建,最后也由程序员释放,如果程序员不释放,程序结束的时候可能由OS回收。(堆区域管理一般采用链表进行管理,程序员申请内存的时候,操作系统会遍历这个空闲内存地址的链表,找到一个合适地址,就把这个节点删除,并将其指向的内存地址分配给程序)
//main.cpp
int a = 0; // 全局初始化区
char *p1; // 全局未初始化区
main {
int b; // 栈
char s[] = "abc"; // 栈
char *p2; // 栈
char *p3 = "123456"; // 123456\0在常量区,p3在栈上
static int c =0; // 全局静态初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20); // 分配得来的10 和20 字节的区域就在堆区
strcpy(p1, "123456"); // 123456\0在常量区,这个函数的作用是将"123456" 这串字符串复制一份放在p1申请的10个字节的堆区域中。
// p3指向的"123456"与这里的"123456"可能会被编译器优化成一个地址。
}
- 数据段:保存全局变量的值
- 代码段:进程执行需要的代码
- 内核部分:当进程由于中断和系统调用进入内核之后,会使用内核的栈。
进程的控制块和上下文切换
- 在内核中,每个进程都有一个进程控制块来保存其相应的状态,,比如进程标识符(PID),进程状态,虚拟内存状态,打开的文件等,这个数据结构为进程控制块。
- 进程上下文包括进程运行时寄存器的状态,用于保存和恢复进程在处理器上运行的状态。
- 上下文切换过程:
父子进程的拷贝
fork
- Linux系统中,进程一般是通过调用fork() 接口,从已经有的进程中分裂出去的。
- fork()接口没有参数,只是返回当前值的PID。
- fork()完成时,两个进程的内存,寄存器,程序计数器等状态完全一样,但是他们是两个独立的进程,拥有不同的PID和虚拟内存空间,并且他们互不干扰。
- 父子进程的执行顺序是不确定的,完全取决于调度器的决策。
- 每个进程在运行过程中都会维护一张已经打开的文件描述符(操作系统对引用某一文件的抽像) 。
- 文件描述符使用偏移量记录当前进程读取到某一文件的某个位置。
- fork()的过程中,由于文件描述符时PCB的一部分,所以会指向相同的文件抽象,与父进程用同一个偏移量。并且Linux在实现read操作时,会对文件加锁,所以父子进程读不到一样的字符串。
- 操作系统中有0号进程。(由操作系统创建的)。
写时拷贝
- fork()之后出现的时两个一毛一样的进程,但是往往我们需要其执行不同的任务,所以就有了exec()这个接口。
- fork()之后,exec()之前,两个进程的内存空间是同一个,但是虚拟空间(在磁盘上是两块区域),当父子进程中有发生改变的时候,在为子进程分配物理内存空间。
- 如果没有exec(),内核会给子进程的数据段、堆栈段分配相应的物理空间,代码和代码库都是用父进程的。
- 如果调用的exec(),内核会给子进程的代码段分配独立的内存空间(功能不一样了,代码肯定不一样了)。
- fork()之后
- 父子进程中有一个要改变
- 由于exec()导致的父子进程的不同,就是执行不同的功能,所以代码段就不能共享了
- 就会如下图所示
进程之间的监控(wait())
- wait()不仅起监控作用,还起到回收进程的作用
- 僵尸进程:如果父进程没有进行wait()操作,,就算子进程已经终止了,它占用的资源也不会释放,,这种进程为僵尸进程。(本来该死,结果自己又跳出来了,确实像僵尸)
- 如果一个进程创建了大量的子进程确从不调用wait(),僵尸进程就会占用可以使用的PID,导致资源不足。
- 如果父进程退出了,所有僵尸进程将会由0号进程通过wait的方式进行回收。
- 孤儿进程:父进程结束了,但是子进程没有结束。