文章目录
进程状态
当一个进程开始运行时,它可能会经历下面这几种状态:
-
运行态:运行态指的就是进程实际占用CPU时间片运行时
-
就绪态:就绪态指的是可运行,但因为其他进程正在运行而处于就绪状态
-
阻塞态︰该进程正在等待某一事件发生(如等待输入/输出操作的完成)而暂时停止运行,这时即使给它CPU控制权,它也无法运行
状态切换: -
运行态到阻塞态:当进程遇到某个事件需要等待时会进入阻塞态
-
阻塞态到就绪态:当进程要等待的事件完成时会从阻塞态变到就绪态
-
就绪态到运行态:处于就绪态的进程被操作系统的进程调度程序选中后,就分配CPU开始运行
-
运行态到就绪态:进程运行过程中,分配给它的时间片用完后,操作系统会将其变为就绪态,接着从就绪态的进程中选择一个运行
程序调度指的是,决定哪个进程优先被运行和运行多久,这是很重要的一点。已经设计出许多算法来尝试平衡系统整体效率与各个流程之间的竞争需求。
进程控制块PCB
每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,
Linux内核的进程控制块是task_struct结构体。其内部成员有很多,以下是重点部分:
- 进程id,每个进程有唯一的id,用pid_t类型表示,其实就是一个非负整数
- 进程的状态,有就绪、运行、挂起状态
- 描述虚拟地址空间的信息
- 文件描述符表,包含很多指向file结构体的指针进程切换时需要保存和恢复的一些CPU寄存器
- 描述控制终端的信息
- 当前工作目录
- umask掩码
- 和信号相关的信息
- 用户id和组id
- 会话(Session)和进程组
- 进程可以使用的资源上限(Resource Limit)
创建进程
创建进程的方式:
- 系统初始化:启动操作系统时会创建若干个进程
- 用户请求创建:例如双击图标启动程序
- 系统调用创建:一个运行的进程可以发出系统调用创建新的进程帮助其完成工作
fork函数
函数描述:
创建进程,新创建的是当前进程的子进程。
函数原型:
pid_t fork(void);
函数返回值:
- 成功:
- 父进程:返回新创建的子进程id
- 子进程:返回0
- 失败返回-1,子进程不被创建
创建多个进程
创建5个进程(包括父进程)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<fcntl.h>
int main(int argc, char* argv[]){
int pid;
for(int i=0;i<4;i++){
pid=fork();
if(pid==0){
break;
}else{
printf("1\n");
}
}
while(1);
}
1
1
1
1
fork()函数会创建一个子进程,父进程的内容会复制到子进程的进程空间中,包括父进程的数据段和堆栈段,并且和父进程共享代码段,所以成功后父子进程都停留在了进程创建函数(fork)上,因此,fork()函数在父子进程中都会返回,两个返回值不同。
父子进程间遵循读时共享写时复制copy-on-write的原则。现在的Linux内核在fork()函数时往往在创建子进程时并不立即复制父进程的数据段和堆栈段,而是当子进程修改这些数据内容时复制操作才会发生,内核才会给子进程分配进程空间,将父进程的内容复制过来,然后继续后面的操作,这样的实现更加合理,对于那些只是为了复制自身完成一些工作的进程来说,这样做的效率会更高这也是现代操作系统的一个重要的概念-----“写时复制"的一个重要体现。
父子进程的局部变量、全局变量、堆区空间不是共享的,父子进程打印变量的地址是相同的(该地址是虚拟地址空间),但是他们指向的物理空间是不同的。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<fcntl.h>
int a=10;//全局变量
int main(int argc, char* argv[]){
int pid=fork();
if(pid==0){
a+=1;
printf("child_pid=%d,parent_pid=%d,&a=%p,a=%d\n",getpid(),getppid(),&a,a);
}else{
sleep(2);
printf("parent pid=%d,&a=%p,a=%d\n",getpid(),&a,a);
}
}
child_pid=22641,parent_pid=22640,&a=0x55793cc99010,a=11 parent
pid=22640,&a=0x55793cc99010,a=10
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<fcntl.h>
int main(int argc, char* argv[]){
char* s=(char*)malloc(10);//堆区空间
int pid=fork();
if(pid==0){
strcpy(s,"hello");
printf("parent_pid=%d,sp=%p,s=%s\n",getppid(),s,s);
}else{
sleep(1);
printf("pid=%d,sp=%p,s=%s\n",getpid(),s,s);
}
free(s);
}
parent_pid=23030,sp=0x55e663a812a0,s=hello
pid=23030,sp=0x55e663a812a0,s=
为什么父子进程共享文件描述符?
每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。
父子进程间文件共享,执行fork()子进程会获得父进程所有文件描述符的副本,这些副本的创建类似于dup(),同一文件描述符在父子进程中对应的是相同的文件。
如果一个进程打开了一个文件以后,创建子进程,那么子进程会继承父进程的环境和上下文中的大部分内容,包括文件描述符。此时父子进程享有相同的文件偏移量。
相当于2个 fd指向同一块内存空间,因为2个进程共享了文件指针偏移量,所以都能向文件中有序写数据
(注意,如果父进程先创建子进程,然后父进程再打开一个文件,那显然,子进程不会有这个文件描述符)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<fcntl.h>
int main(int argc, char* argv[])
{
int fd=open("./a.txt",O_RDWR);
int pid=fork();
if(pid==0){
int wct=write(fd,"Hello",5);
printf("childPro,wct=%d\n",wct);
}
if(pid>0){
char buf[1024];
int rct=read(fd,buf,sizeof(buf));//读取不到数据,文件偏移量已在写入文件时改变,可设置文件偏移量读取到数据
printf("fatherPro,rct=%d,buf=%s\n",rct,buf);
}
return 0;
}
终止进程
可使用以下两种方式之一来终止一个进程:
- 其一,进程可使用_exit()系统调用(或相关的exit()库函数),请求退出;
- 其二,向进程传递信号,将其“杀死”。
无论以何种方式退出,进程都会生成“终止状态”,一个非负小整数,可供父进程的wait()系统调用检测。
在调用_exit()的情况下,进程会指明自己的终止状态。
若由信号来“杀死”进程,则会根据导致进程“死亡”的信号类型来设置进程的终止状态。(有时会将传递进_exit()的参数称为进程的“退出状态”,以示与终止状态有所不同,后者要么指传递给_exit()的参数值,要么表示“杀死”进程的信号。)
根据惯例,终止状态为0 表示进程“功成身退”,非0 则表示有错误发生。大多数shell 会将前一执行程序的终止状态保存于shell 变量$?中。
进程终止的方式:
-
正常退出:
- 从main函数返回
- 调用exit()或_exit(),exit()是库函数,_exit()是系统调用,程序一般不直接调用_exit(),而是调用库函数exit()。
-
异常退出:
- 被信号终止
exit函数
函数描述:
结束进程
头文件:
#include <stdlib.h>
函数原型:
void exit(int status);
函数参数:
进程的退出状态,0表示正常退出,非0值表示因异常退出,保存在全局变量$?中,$?保存的是最近一次运行的进程的返回值,返回值有以下3种情况:
- 程序中的main 函数运行结束,$?中保存main函数的返回值
- 程序运行中调用exit函数结束运行,$?中保存exit函数的参数
- 程序异常退出$?中保异常出错的错误号
int main(int argc, char* argv[]){
printf("exit\n");
exit(100);// return 100;
}
孤儿进程
孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,谁会是孤儿(orphan)子进程的父进程?进程ID 为1 的众进程之祖—init 会接管孤儿进程。换言之,某一子进程的父进程终止后,对 getppid()的调用将返回 1.
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<fcntl.h>
int main(int argc, char* argv[])
{
int pid=fork();
if(pid==0){
while(1){
printf("child pid=%d,parent pid=%d\n",getpid(),getppid());
sleep(1);
}
}
sleep(8);
printf("parent pid=%d\n",getpid());
}
ps ajx | grep ./main查看进程:
僵尸进程
在父进程执行 wait()之前,其子进程就已经终止,子进程已经结束,系统仍然允许其父进程在之后的某一时刻去执行 wait(),以确定该子进程是如何终止的。
内核通过将子进程转为僵尸进程(zombie)来处理这种情况。这也意味着将释放子进程所把持的大部分资源,以便供其他进程重新使用。该进程所唯一保留的是内核进程表中的一条记录,其中包含了子进程ID、终止状态、资源使用数据等信息。无法通过信号来杀死僵尸进程.
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<sys/types.h>
int main(int argc, char* argv[])
{
int pid=fork();
if(pid==0){
sleep(5);
printf("child pid=%d,parent pid=%d\n子进程终止\n",getpid(),getppid());
exit(0);
}
sleep(20);
printf("parent pid=%d\n",getpid());
while(1);
}
进程回收
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:
- 如果是正常终止则保存着退出状态
- 如果是异常终止则保存着导致该进程终止的信号是哪个。
这个进程的父进程可以调用wait 或 waitpid 获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell 中用特殊变量$?查看,因为Shell是它的父进程,当它终止时 Shell调用wait或 waitpid得到它的退出状态同时彻底清除掉这个进程。
wait函数
函数描述
父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
- 阻塞等待子进程退出
- 回收子进程残留资源
- 获取子进程结束状态(退出原因)。
头文件:
#include <sys/types.h>
#include <sys/wait.h>
函数原型:
pid_t wait(int *status)
函数参数:
status为传出参数,用以保存进程的退出状态
函数返回值:
- 成功:返回清理掉的子进程ID;
- 失败:返回-1(没有子进程)
当进程终止时,操作系统的隐式回收机制会:
1.关闭所有文件描述符
2.释放用户空间、分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。
(正常终止→退出值;异常终止→终止信号)
wait回收子进程(阻塞回收)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>