进程创建
以父进程为模板创建子进程,复制父进程的pcb大部分数据(共用代码段,数据独有,有自己的虚拟地址空间)。
fork
实现:
在fork()函数执行结束之后,其返回值是存放在eax寄存器中的,在子进程中fork()的结尾处,置0。造成子进程也执行一次fork()的假象。
#include <unistd.h>
pid_t fork(void);
返回值:
- 子进程返回0
- 父进程返回子进程的pid
测试代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
printf("pid:%d\n",getpid());
if((pid=fork()) == -1){
perror("fork() Error");
exit(1);
}
printf("pid: %d, fork return: %d\n",getpid(), pid);
return 0;
}
样例执行结果:
父进程pid:10405,fork后返回的是子进程pid:10406;
子进程pid:10406,fork后返回的是 0。
能否子进程返回父进程pid,父进程返回0?
不可以,因为父进程不知道自己有多少个子进程,每个子进程创建成功后就独立于父进程,所以无法知道子进程id。而子进程可以通过getppid()来获取父进程id。此外,通过返回值可以用明确的方法来分辨程序是在父进程还是子进程中执行。
- 写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的;
以任意方式试图写入,便以写时拷贝的方式给子进程一份副本。
fork常规用法
- 一个父进程创建子进程,希望子进程运行另一个程序;
- 执行不同代码段。
注意:当系统进程数超出限制可能会fork失败。
vfork(被淘汰了)
vfork创建出来的子进程,与父进程共用同一虚拟空间。
1. 子进程没有运行其他程序(exec)或者退出之前,父进程阻塞在vfork处不返回意味着子进程是先运行的,子进程退出(不能在main函数return)之后,父进程才能运行;
2. 子进程先运行的原因: 因为创建子进程,大多是为了让他运行其它程序;
3. 父进程阻塞原因: vfork创建子进程共用一块虚拟地址空间(操作相同数据)。他们共用同一栈区可能造成调用栈混乱。
vfork设计目的:为了创建一个子进程,然后直接运行其他程序。重新运行其他程序就是重新给子进程开辟新的空间,更新它自己的一份地址空间和页表。
进程终止
正常终止(echo $? //查看进程退出码): 返回值只有一个字节(0 ~ 255 )保存退出状态
- main函数中return退出;
- exit 库函数调用
- _exit
exit 系统调用接口:
#include <stdlib.h>
void exit(int status); //status进程退出状态
- 在进程任意位置调用exit都会退出进程。
- 在main函数return和任意位置调用exit效果一样。
_exit 系统调用接口:
#include <unistd.h>
void _exit(int status);
#include <stdlib.h>
void _Exit(int status);
exit与_exit的区别:
- exit温和性的退出,退出进程前会温和的释放资源(逐步释放资源),刷新缓冲区;
- _exit暴力推出,直接释放资源,不会刷新缓冲区,不会关闭文件等操作。
不管哪种主动退出方式,都会返回一个数字,这个数字就是进程退出状态,表明了进程的退出原因。
1.正常运行完毕,结果符合预期;
2. 正常运行完毕,结果不符合预期;
3. 异常退出,返回状态不能作为评判标。
进程等待
一个进程退出以后,因为要保存自己退出原因,因此并不会释放所有资源,它等着父进程查看它退出原因,然后释放所有资源。
假如父进程根本没有管,那么这个子进程就变成了僵尸进程,就造成了资源泄露,因此为防止出现僵尸进程,父进程就应该关心子进程的退出。
这就要求父进程一直关注子进程的状态: 通过wait函数来完成这项工作。
进程等待就是等待子进程状态改变,获取子进程的退出状态码,允许系统释放子进程的所有资源。也是避免产生僵尸进程的主要方式。
进程等待的方式:
- wait
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
- status 用于获取子进程退出状态码,不关心可以设置为NULL。
- 返回值 是返回退出的子进程 pid,失败返回-1。
如果没有子进程则直接报错:
- waitpid
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
- pid: >0 表示等待与pid相同的进程ID,为-1时与wait作用一样。
- status:
WIFEXITED(status):若为正常终止子进程返回状态则为真。<查看进程是否正常退出>
WEXITSTATUS(status):结果非零,提取子进程退出码。<查看子进程退出码>
- options:
WNOHANG:若等待的子进程未退出,返回0不再等待;若正常退出返回子进程pid
- 返回值:
正常执行返回子进程pid;
设置了WNOHANG,子进程还在运行则返回0;
调用中出错返回-1.
status通过位图实现
其低16位结构:
说明:
wait获取的状态(status),使用了两字节,这两字节中:
- 高8位存储进程退出时返回的状态码
- 低7位存储的是引起进程异常退出的信号
- 还有中间1位(低8位的最高位),是core dump标志
- 阻塞式等待:
#include <stdio.h>
#include <stdlib.h> #include <sys/wait.h>
int main()
{
pid_t pid = fork();
if(pid < 0){
perror("fork error");
exit(1);
}
else if(pid == 0){
printf("This is child process, pid: %d\n",getpid());
sleep(10);
exit(2);
}
else{
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
printf("Waitting for chlid process\n");
if(WIFEXITED(status) && ret == pid){
printf("wait sucess, child return status: %d\n", WEXITSTATUS(status));
}
else{
printf("this time failed\n");
return 1;
}
}
return 0;
}
运行结果:
- 非阻塞式等待:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
{
pid_t pid = fork();
if(pid < 0){
perror("fork error");
exit(1);
}
else if(pid == 0){
printf("This is child process, pid: %d\n",getpid());
sleep(10);
exit(2);
}
else{
int status = 0;
pid_t ret = 0;
do{
ret = waitpid(-1, &status, WNOHANG);
printf("chlid process running\n");
}while(ret == 0);
if(WIFEXITED(status) && ret == pid){
printf("wait sucess, child return status: %d\n", WEXITSTATUS(status));
}
else{
printf("this time failed\n");
return 1;
}
}
return 0;
}
运行结果:
类似的每隔10s打印一次字符串。
先通过top命令查看运行进程pid:
可以通过杀死进程。
然后得到子进程退出信息:
程序替换
举例:
假如shell中执行的 ls 命令
- 输入 ls ,然后回车
- shell 接收我们输入的信息
- shell 去指定的目录下找 ls 这个程序 // ls 是外部命令 并不是所以命令都是外部命令,也有shell内建命令。
- 找到 ls 程序之后,shell 创建一个子进程
- 将子进程的代码段给替换掉
程序替换: 相当于让虚拟地址空间中代码段地址指向了物理内存的另一代码位置。
这样的话虚拟地址空间中原先的数据区域以及堆栈都会重新初始化,因为现在代码运行的根本不是复制那些数据。但是这个进程( pcb) 还是原来的进程(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[]); // 类似execl
- int execvp(const char *file, char *const argv[]); // 类似于execv
- int execve(const char *file, char *const argv[], char *const envp[]);
说明:
- path 程序的全路径名
- file 指定路径 即就是PATH(环境变量)
l 与 v 区别:l 是参数平铺一个一个通过exec 函数参数赋予,v参数直接使用字符串指针数组execl/execv 需要我们给出替换的程序的全路径名。
记忆方法:
- l(list):参数地址列表,以空指针结尾。
- v(vector):存有各参数地址的指针数组地址,使用时先构造一个指针数组,指针数组 存各参数的地址,然后奖该指针数组作为函数的参数。
- p(path):按系统环境变量 PATH 指定的目录去搜索可执行文件。文件名由第一个参数 file 指定,当指定的文件名中包含/,则将其视为路径名,并直接到指定的路径中执行 程序。
- e(environment):存有环境变量字符串地址的指针数组首地址。 execle 和 execve 改 变的是 exec 启动的程序的环境变量(新的环境变量完全由 environment 指定)。