创建进程:
pid_t fork(void)
创建一个新的进程(子进程),父进程调用fork()后,系统会为子进程分配资源:创建了一个新的PCB,然后将父进程PCB的大部分数据复制到新的PCB。PCB里边都有什么内容呢分为这么几类:
(1)进程的标识信息(执行的哪个可执行文件,id是多少,父进程id等
(2)进程的状态信息(CPU状态寄存器里的信息,地址空间,页表起始地址,进程状态,是否允许调度等)
(3)进程占用的资源(分配给它的内存(内存地址空间mm),内核堆栈信息等)
(4)保护现场的信息(寄存器信息等上下文信息)程序计数器/上下文数据:子进程被创建出来后,运行位置与父进程是完全一样的,下一步即将执行的指令都是从创建子进程成功后开始的。
如何区分父进程与子进程
pid:进程(ID)是大多数操作系统的内核用于唯一识别进程的一个数值。父子进程代码共享,数据独有,当调用fork函数创建子进程时,父进程的会返回子进程的pid,子进程会返回 0;所以虽然父子进程的代码一样,我们可通过判断fork的返回值不同实现父子进程的代码分流。
实现代码如下:(getpid函数用来获得调用进程的pid,谁调用返回谁的pid。)
#include<stdio.h>
#include<unistd.h>
int main(){
printf("creat process start\n");
pid_t pid;
pid = fork();
if(pid == 0){
//通过fork返回值的实现父子进程的代码分流。
printf("this is child--rpid:%d pid:%d \n",getpid(),pid);
}else if(pid > 0) {
printf("this is parents--rpid%d pid:%d \n",getpid(),pid);
}else{
//出错
}
printf("creat process end--- %d\n",getpid());
return 0;
}
写时拷贝技术
fork创建子进程的特点是数据独有代码共享,可是他的PCB,页表,虚拟地址空间。都是从父进程复制而来的,也就是跟父进程用的代码。那么他又是如何做到数据独有的呢?这里就用到了写时拷贝技术了。
子进程除了复制了父进程的PCB大部分数据,(虚拟地址,页表等),所以一开始子进程与父进程的数据使用同一块物理内存,但是当这块物理内存的数据发生改变时会重新给子进程开辟物理内存,并将数据拷贝过来。如图所示:假如fork之前程序就创建了变量gval,fork后程序分流,如果子进程要改变gval的值。就会在内存重新开辟一个空间,并将gval 的值拷贝过来。并改变页表的相应映射关系改变,这样子进程就拥有了自己的gval。这就是数据独有代码共享。
为什么不直接在子进程创建的时候给子进程重新开辟物理内存空间并拷贝所有数据呢?这样做是为了提高效率,因为创建时直接开辟空间并拷贝数据,将会比较缓慢,并且拷贝过来的数据子进程可能从来都不会使用。
进程退出:
(1)在main函数中return.退出时会刷新缓冲区
(2)调用库函数exit() 退出前会刷新缓冲区
(3)调用系统调用接口_ exit() 退出时直接释放资源,不刷新缓冲区
以上的几种方式都属于正常退出,进程也有可能异常退出比如程序崩溃
进程等待:
进程等待:父进程等待子进程的退出,获取子进程的返回值,避免产生僵尸进程。
*pid_t wait(int status)
阻塞等待任意一个子进程退出,获取子进程的返回值放到status指向的空间中,并且释放子进程资源;返回退出的子进程pid。
阻塞:为了完成某个功能发起一个调用, 若当前不具备完成功能的条件,则阻塞。直到条件满足,完成功能。
非阻塞:为了完成某个功能发起- -个调用, 若当前不具备完成功能的条件,则立即报错返回;非阻塞操,通常需要循环操作。
*pid_ t waitpid(pid_ t pid, int status, int options)
pid:若==-1,则表示等待任意- 个子进程退出,若是>0, 则表示等待指定的子进程退出
status:输出型参数,传入-个int空间的首地址,获取退出的子进程返回值
options:选项参数,0–则表示默认阻塞等待; WNOHANG–将waitpid设置为非阻塞- -没有子进程已经退出的话就立即报错返回
返回值:若等待到了子进程退出则返回子进程的pid;若有子进程,但是没有退出则返回0;出错返回-1
wait和waitpid并不是只处理刚退出的进程,而是对已经退出的进程进行处理(不管什么时候退出的)。
如何判断子进程是否正常退出?
core dump :核心转储–程序异常退出时, 则保存程序的运行信息,便于事后调试
信号:
通知进程发生了某个异常事件,中断进程当前的操作,去处理这个事件结论:而操作系统中的信号的体现实际是一一个数字, 一个数字对应一个异常事件,程序因为异常退出的事后,则会将这个异常退出事件的信号值放到低7位中
结论:status低七位为零表示正常退出,否则表示异常退出,而当进程异常退出时,获取进程的返回值毫无意义,所以应该先要获取低7位,异常退出信号值,通过是否为0判断是否进程正常退出。
00000000000000 0000000 0111 1111 & status=低7位 (0x7f & status)用位运算有点麻烦,系统提供了宏WIFEXITED(status) – 用于根据status判断子进程是否正常退出。WEXITSTATUS(status) --从status中取出子进程退出返回值
程序替换:
- 程序替换的概念:
替换一个进程正在运行的程序,重新加载一个新的程序到物理内存中,对一个进程的代码通过页表在物理内存中的地址进行修改映射关系,让程序的代码经过页表转换后,指向了新的程序位置。
让一个进程pcb通过页表转换映射到物理内存上另一个程序的地址,进程将运行另一个程序,以前的数据和代码都失效了。
- exec族函数实现程序替换
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
const char *path: 新程序的路径(就是使用路径下的程序替换正在运行的程序)
const char *arg: 将新程序的运行参数,通过不定参的形式传递进入新的程序,以NULL作为结尾。
char* argv[] = {"ls","-l"};
execv("/usr/bin/ls",argv);//调用ls程序
execl("/usr/bin/ls","ls","-l",NULL);//对当前的程序进行程序替换,去运行ls程序
l和v的区别:在于程序运行参数的赋值方式不同,l通过不定参完成,v通过字符串指针数组进行赋值。
有没有p的区别:在于第一个参数执行新程序的时候,是否需要带路径,有p则可以不用带路径,默认回去Path环境变量指定的路径下查找。
有没有e的区别:在于这个进程的环境变量是否需要重新初始化;有e则表示初始化,若没有e则表示使用默认的环境变量。