文章目录
- 一.进程创建
- 写时拷贝
- fork常规用法
- fork调用失败的原因
- 二.进程终止
- 进程退出场景
- 进程常见退出方法
- 三.进程等待
- 进程等待必要性
- 进程等待的方法
- wait方法
- waitpid方法
- 获取子进程status
- 非阻塞式等待
- 进程程序替换
- 替换原理
- 替换函数
- 简易shell的实现
一.进程创建
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程,fork函数初步使用在Linux进程概念中已经给出,现在我们再来深入认识一下
#include <unistd.h>
pid_t fork(void);
// 返回值:自进程中返回0,父进程返回子进程id,出错返回-1
(1). 为何要给子进程返回0,给父进程返回子进程的pid
父进程可以有多个子进程,子进程只有一个父进程,这也意味着父进程寻找子进程需要得到子进程的标识,而子进程寻找父进程则不需要标识(因为子进程只有一个父进程) ; 父进程将任务分配给多个子进程去执行,父进程需要知道每个进程的pid来知道每个进程执行的是哪个任务(给父进程返回子进程的pid),而子进程不关心父进程的情况(给子进程返回0)
(2). 如何理解fork有两个返回值的问题?
进程调用fork,当控制转移到内核中的fork中的代码,内核做以下几件事情 :
1). 创建task_struct,mm_struct,页表等内核数据结构
2). 将父进程的代码和数据拷贝至子进程
3). 将子进程添加到系统的进程列表,cpu的运行队列中
我们需要理解的是并不是fork之后才会创建出子进程,实际上在执行内核中的fork代码时,做完上面的几件事情之后子进程就已经被创建好了,而不是等fork代码执行完才创建出子进程,因为在fork代码执行完之前就有了两个执行流,所以最终会有两个返回值
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图
共享 : 父子进程对应的页表指向的是同一块物理内存
(1). 为什么要进行写时拷贝 ?
因为进程具有独立性
(2). 为什么不在创建子进程时就将父子进程数据进行分离呢?
a. 因为子进程不一定会使用父进程所有的数据,且子进程不一定会对数据进行写入操作(有可能只是进行读取操作)
b. 延时分配 : 一个子进程刚刚被创建出来的时候,并不一定会被立即调度,当进程被调度时,再为其分配物理内存空间,可以使物理内存空间利用率达到最大化
(3). 代码会不会进行写时拷贝 ? 90%不会,但并不代表不能
fork常规用法
(1). 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
(2). 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数 ; bash进程创建子进程,子进程去执行程序
fork调用失败的原因
(1). 系统中有太多的进程
(2). 实际用户的进程数超过了限制
二.进程终止
进程退出场景
我们在Linux进程概念中提到了 echo $? 命令,该命令打印出最近一次进程的退出码
(1). 代码运行完毕,结果正确
(2). 代码运行完毕,结果不正确
(3). 代码异常终止
#include<stdio.h>
int main()
{
int a = 1 / 0;
printf("%d\n",a);
// int* p;
// *p = 100;
}
退出码为0只有一种情况 : 代码运行完毕,结果正确
退出码非0表示程序有问题,导致问题的原因有多种可能
我们可以使用 strerror 函数来查看退出码对应的信息,每个退出码都有对应的信息,帮助用户确认任务失败的原因
// test.c内容
#include<stdio.h>
#include<string.h>
int main()
{
for(int i = 0;i <= 133;i++) // 一共134个退出码
{
printf("%d : %s\n",i,strerror(i));
}
return 0;
}
退出码对应的相关信息如下图 :
进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):
(1). 从main返回(只有main函数的return才能结束进程)
(2). 调用exit
(3). _exit
exit 和 _exit 的区别 : exit会释放进程曾经占用的资源(例如,缓冲区),_exit直接终止进程,不会做任何收尾工作
#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main()
{
printf("hello world!");
sleep(3);
exit(10);
printf("\n.......................................\n");
sleep(3);
}
现象 : 先睡眠3秒,再打印出 hello world! ,原因是 hello world! 缓存到输出缓冲区中,由于采用的是行缓冲,没有遇到\n刚开始没有刷新出来,睡眠3秒后,执行exit(10),进程退出,将输出缓冲区的内容刷新出来
同样的代码,如果将 exit(10) 换成 _exit(10),现象为睡眠3秒后,进程退出,什么都不输出,原因是 执行_exit(10),进程退出,不会刷新缓冲区的内容
异常退出:本质上是进程运行的时候出现了某种错误,导致进程收到信号
(1). ctrl + c,信号
(2).
// test.c内容
#include<stdio.h>
int main()
{
int a = 1 / 0;
printf("%d\n",a);
// int* p;
// *p = 100;
}
查看退出码为 136 ,而退出码最大为133,由此我们可知当进程异常退出时,它的退出码就没有意义了
进程终止,操作系统做了什么?
释放曾经申请的数据结构,释放曾经申请的内存,从各种队列等数据结构中移除
三.进程等待
进程等待必要性
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法
杀死一个已经死去的进程。最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的方法
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
// test.c 内容
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 0;
while(count < 10)
{
printf("I am a child,pid : %d,ppid : %d\n",getpid(),getppid());
count++;
sleep(1);
}
exit(0);
}
else
{
printf("I am a father,pid : %d,ppid : %d\n",getpid(),getppid());
pid_t ret = wait(NULL);
if(ret >= 0)
{
printf("wait child success!,%d\n",ret);
}
printf("father is run.............\n");
sleep(10);
}
}
(1). 在子进程运行期间,父进程wait的时候,就是在等子进程退出,这种状态叫做阻塞等待
(2). 父子进程谁先运行不确定,但是wait以后,大部分情况都是子进程先退出,父进程读取子进程退出信息,父进程才退出
(3). 父进程等待成功(只能意味着子进程退出了),并不意味着子进程运行成功
waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。
(1). 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
(2). 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则父进程可能阻塞。
(3). 如果不存在该子进程,则立即出错返回。
获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特
位):
低7位,代表进程退出时的退出信号,次低8位代表进程退出的退出码
// test.c内容
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 0;
while(count < 10)
{
printf("I am a child,pid : %d,ppid : %d\n",getpid(),getppid());
//if(count == 10)
//{
// int a = 1 / 0; // 收到8号信号,浮点数报错(SIGFPE)
// int *p;
// *p = 100; // 收到11号信号,段错误
//}
count++;
sleep(1);
}
exit(10);
}
else
{
printf("I am a father,pid : %d,ppid : %d\n",getpid(),getppid());
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret >= 0)
{
printf("wait child success!,%d\n",ret);
printf("status : %d\n",status);
printf("child get signal : %d\n",status & 0x7F);
printf("child exit code : %d\n",(status >> 8) & 0xFF);
}
printf("father is run.............\n");
sleep(10);
}
}
正常退出情况
异常退出情况
我们也可以不使用位运算来获取信号和退出码,可以使用宏
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
// 本质是检查信号,若信号为0,说明正常终止,信号非0,说明异常终止
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
// 若正常终止,则可以获取退出码
if(WIFEXITED(status)
{
printf("child exit code : %d\n",WEXITSTATUS(status));
}
else
{
printf("child not exit normal!\n");
}
创建多个子进程,父进程进行等待
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t ids[10];
for(int i = 0;i < 10;i++)
{
pid_t id = fork();
if(id == 0)
{
int count = 0;
while(count < 10)
{
printf("I am a child, pid : %d,ppid : %d\n",getpid(),getppid());
sleep(1);
count++;
}
exit(i);
}
ids[i] = id;
}
int count = 0;
while(count < 10)
{
int status = 0;
pid_t ret = waitpid(ids[count],&status,0);
if(ret >= 0)
{
printf("wait child success!,%d\n",ret);
printf("status : %d\n",status);
if(WIFEXITED(status))
{
printf("child exit normal!\n");
printf("child exit code : %d\n",WEXITSTATUS(status));
}
else
{
printf("child not exit normal!\n");
printf("child get signal : %d \n",status & 0x7F);
}
}
count++;
}
}
非阻塞式等待
实现非阻塞式等待,需要将waitpid的第三个参数设置为WNOHANG,非阻塞式等待和阻塞式等待的区别为非阻塞式等待不会死等子进程退出,当检测到子进程还没有退出,返回0,然后父进程就可以先去做别的事情,过一段时间再检测子进程是否退出
// test.c 内容
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 0;
while(count < 10)
{
printf("child,pid : %d,ppid : %d\n",getpid(),getppid());
sleep(3);
count++;
}
exit(1);
}
while(1)
{
int status = 0;
pid_t ret = waitpid(id,&status,WNOHANG);
if(ret > 0)
{
if(WIFEXITED(status))
{
printf("child exit normal!\n");
printf("child exit code : %d\n",WEXITSTATUS(status));
}
else
{
printf("child not exit normal!\n");
printf("child get signal : %d\n",status & 0x7F);
}
break;
}
else if(ret == 0)
{
printf("father do other things!\n");
sleep(1);
}
else
{
printf("waitpid error\n");
break;
}
}
exit(0);
}
进程程序替换
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的pid并未改变(只是先前的代码和数据被替换成了磁盘中的新的代码和数据)
(1). 在我们的程序里可以使用execl函数让我们的程序不再执行,去执行别的程序(将磁盘中的程序加载到内存)
(2). 进程程序替换,一经替换,不再返回,后续代码不会执行
(3). 程序替换失败,会返回,程序后续不会受影响
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("I am a child process!\n");
sleep(3);
// Linux下的加载器所采用的底层技术,让我们的程序不再执行,去执行别的程序
execl("/usr/bin/ls","ls","-l","-a",NULL);
exit(10);
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
if(WIFEXITED(status))
{
printf("child exit normal!\n");
printf("child exit code : %d\n",WEXITSTATUS(status));
}
else
{
printf("child not exit normal!\n");
printf("child get signal : %d\n",status & 0x7F);
}
}
else
{
printf("waitpid error!\n");
}
}
替换函数
其实有六种以exec开头的函数,统称exec函数 :
#include <unistd.h>
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[]);
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
前面的5个函数实际上底层都是去调用execve系统调用,可以理解为前面的5个函数是对execve系统调用进行了封装
int execve(const char *path, char *const argv[], char *const envp[]);
// 使用
execl("/usr/bin/ls","ls","-a","-l","-i",NULL);
execlp("ls","ls","-a","-l","-i",NULL);
char* myargv[] = {"ls","-a","-l","-i",NULL};
execv("/usr/bin/ls",myargv);
execvp("ls",myargv);
char* myenvp[] = {"MYVAL=hello world!",NULL};
execle("./test2","./test2",NULL,myenvp);
execve("/usr/bin/ls",myargv,myenvp);
// 例子
#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
(1). 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
(2). 如果调用出错则返回 -1
(3). 所以exec函数只有出错的返回值而没有成功的返回值。
(4). exec函数也可以调我们自己写的程序(参考如下代码)
(5). execle函数导环境变量是覆盖式的,自己导入的环境变量会覆盖掉默认的环境变量
// Makefile
.PHONY:all
all:test2 test
test2:test2.c
gcc -o $@ $^
test : test.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f test2 test
// test2.c
#include<stdio.h>
int main()
{
printf("I am a process!\n");
// execle函数的使用,可以对比一下使用execle函数导入环境变量前后的变化
//printf("my env : %s\n",getenv("MYVAL"));
//printf("OS env : %s\n",getenv("PATH"));
}
// test.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("I am a child process!\n");
sleep(3);
execl("./test2","./test2",NULL);
// execle的使用
//char* myenvp[] = {"MYVAL=hello world!",NULL};
//execle("./test2","./test2",NULL,myenvp);
exit(10);
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
if(WIFEXITED(status))
{
printf("child exit normal!\n");
printf("child exit code : %d\n",WEXITSTATUS(status));
}
else
{
printf("child not exit normal!\n");
printf("child get signal : %d\n",status & 0x7F);
}
}
else
{
printf("waitpid error!\n");
}
}
简易shell的实现
系统启动以及用户登录的时候,会自动调用bash程序,将其运行起来,变成进程,当去执行ls , pwd等指令时,会创建子进程去完成该任务
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#define LEN 1024
#define NUM 32
int main()
{
char cmd[LEN];
char* argv[NUM];
while(1)
{
printf("[luanyiping@bogon shell]$");
// 从标准输入中读取指令到cmd中
fgets(cmd,LEN,stdin);
// 将最后的'\n'变成'\0'
cmd[strlen(cmd) - 1] = '\0';
// 以空格为分隔符分割指令
argv[0] = strtok(cmd," ");
int i = 1;
while(argv[i] != strtok(NULL," "))
{
i++;
}
// 创建子进程去完成任务
pid_t id = fork();
if(id == 0)
{
// 进程程序替换
execvp(argv[0],argv);
exit(10);
}
// 阻塞式等待子进程退出
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0)
{
printf("child exit code : %d\n",WEXITSTATUS(status));
}
}
}