目录
一、进程创建
1.进程创建过程
2.写时拷贝
3.fork函数的两种常规用法
二、进程终止
1.进程终止的三种情况
2.进程退出信息
(1)退出码
(2)退出信号
3.进程终止的方式
三、进程等待
1.为什么要有进程等待?
2.进程等待方法
(1)wait函数
(2)waitpid函数
四、进程替换
1.进程替换原理
2.进程替换函数
(1)execl函数
(2)execlp函数
(3)execle函数
(4) execv函数
(5)execvp函数
(6)execve函数
(7)execvpe函数
补充:
一、进程创建
进程创建即使用fork函数创建子进程,fork函数的返回值有三种情况:
对于子进程返回0;对于父进程返回新创建子进程pid;对于进程创建失败返回-1
1.进程创建过程
调用fork函数后,操作系统为子进程开辟一份新空间,将父进程的部分内核数据结构(task_struct,mm_struct,页表)拷贝给子进程,添加子进程到系统调度队列中,fork函数返回,系统开始调度进程。
注意:进程创建一定是先创建其内核数据结构,再加载其代码和数据
2.写时拷贝
子进程会继承父进程的数据,父子进程共用一份数据,当子进程要修改数据时,为了不影响父进程,因此要发生写时拷贝:即将父进程的数据重新拷贝一份,子进程再进行修改。
3.fork函数的两种常规用法
- 使用fork函数创建子进程,使父子进程同时执行不同的代码段,例如父进程等待客户端生成请求,生成子进程处理请求
- 使用fork函数创建子进程,再调用exec函数,使得子进程执行其他进程(进程替换)
二、进程终止
进程终止就是要释放进程加载到内存中的代码和数据,以及进程的内核数据结构。
进程终止通常是先释放其代码和数据,此时进程处于僵尸状态,等待其退出信息被父进程读取后再释放其内核数据结构。
1.进程终止的三种情况
一个进程终止,有三种情况:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
每种情况进程终止时都会返回退出信息
2.进程退出信息
进程退出信息包括:退出码和退出信号
(1)退出码
代码执行完毕,进程会返回退出码,有0和非0,0表示运行结果正确,非0值不同的值有不同的错误描述。使用echo $? 命令查看最近一个子进程的退出码
我们写程序时也可以自定义退出码,不使用系统规定的退出码
#include <stdio.h>
// 自定义退出码
enum{
Success=0,
Div_Zero,
Mod_Zero
};
// 退出描述
const char* Exit_Description(int Exit_Code)
{
switch(Exit_Code)
{
case Success:
return "Success!";
case Div_Zero:
return "div zero!";
case Mod_Zero:
return "mod zero!";
default:
return "unknow error!";
}
}
// 默认退出码
int Exit_Code=Success;
// 除法函数
int Div(int x, int y) {
if (y == 0) { // 除数为0
Exit_Code = Div_Zero;
return -1;
} else {
return x / y;
}
}
int main() {
int result = Div(10, 100);
printf("result: %d [%s]\n", result, Exit_Description(Exit_Code));
result = Div(10, 0);
printf("result: %d [%s]\n", result, Exit_Description(Exit_Code));
return 0;
}
//result: 0 [Success!]
//result: -1 [div zero!]
(2)退出信号
当进程运行异常终止时,会返回退出信号,退出码就无意义了
使用kill -l命令查看所有的退出信号
3.进程终止的方式
- main函数return
- exit函数
- _exit函数
void exit(int status)
头文件:#include <unistd.h> status是自定义的退出码
void _exit(int status);
头文件:#include <unistd.h> status是自定义的退出码
_exit函数是系统调用指令,exit函数是库函数,不管在任何位置调用_exit或者exit,都表示整个进程终止了。exit库函数底层就是调用_exit指令实现的。
exit终止进程时会冲刷缓冲区,_exit终止进程时不会冲刷缓冲区
三、进程等待
1.为什么要有进程等待?
父进程要通过等待,回收子进程资源(僵尸进程),获取子进程退出信息
2.进程等待方法
进程等待通过wait和waitpid函数实现
(1)wait函数
pid_t wait(int*status)
头文件:#include <sys/types.h> #include <sys/wait.h>
等待最近子进程返回,等待成功返回被等待进程pid,等待失败返回-1;
status为输出型参数,用于获取子进程的退出信息,不关心则可设置为NULL。status
是一个32位整型数,使用其低16位来保存退出信息。其中,0~7存储退出信号,8~15存储退出码
(status>>8)&0xFF即为退出码,status&0x7F即为退出信号
wait函数代码演示
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void ChildRun()
{
int i=0;
for(i=0;i<5;++i)
{
printf("Child process is running!\n");
}
}
int main()
{
pid_t id=fork();
//子进程
if(id==0)
{
ChildRun();
exit(0);//子进程退出
}
//父进程
int status=0;
pid_t rid=wait(&status);
if(rid>0)
{
printf("Father process wait success! status:%d, child quit code:%d, child quit signal:%d\n",status,(status>>8)&0xFF,status&0x7F);
}
else
{
printf("Father process wait fialed!\n");
}
return 0;
}
(2)waitpid函数
pid_ t waitpid(pid_t pid, int *status, int options);
头文件和wait函数相同,pid是等待指定进程返回,status是输出型参数,options用于确定阻塞等待还是非阻塞等待
status输出型参数除了可以使用位运算获取退出码和退出信号,还可以使用WIFEXITED和WEXITSTATUS
WIFEXITED(status):查看进程是否正常终止(正常终止返回真,异常终止返回假)
WEXITSTATUS(status):若WIFEXITED非零(进程正常终止),则提取进程退出码
options参数可以使用WNOHANG,来实现非阻塞等待(在等待的过程中,父进程可以去完成其他任务)
WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
如果pid传入-1,options传入0则与wait函数等效
waitpid函数实现非阻塞等待代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
void ChildRun()
{
int i=0;
for(i=0;i<5;++i)
{
printf("Child process is running!\n");
}
}
void DoOtherthing()
{
printf("Child process is not end! Father process is do other thing...\n");
}
int main()
{
pid_t id = fork();
if (id < 0)
{
printf("fork error!\n"); // 子进程创建失败
}
else if (id == 0) // 子进程执行
{
ChildRun();
exit(123);
}
else // 父进程执行
{
int status = 0;
pid_t rid = waitpid(id, &status, WNOHANG); // 非阻塞等待
// 子进程未终止
while (rid == 0)
{
DoOtherthing(); // 父进程做其他事情
rid = waitpid(id, &status, WNOHANG);
}
// 子进程等待失败
if (rid == -1)
{
printf("wait failed!\n");
}
else // 子进程等待成功
{
if (WIFEXITED(status)) // 子进程正常终止
{
printf("wait success! child exit code:%d\n", WEXITSTATUS(status));
}
else // 子进程异常终止
{
printf("wait success! child abnormal exit\n");
}
}
}
return 0;
}
四、进程替换
1.进程替换原理
用fork函数创建子进程后,父子进程执行的是相同的程序。当子进程调用exec函数,子进程要写发生代码和数据的写时拷贝,然后子进程的代码和数据会被新程序的代码和数据替换,这个过程中并没有创建新的进程。
2.进程替换函数
(1)execl函数
int execl(const char *path, const char *arg, ...);
execl中的 l 表示路径,path是参数路径,arg参数传参只需要像Linux命令一样带上选项即可,其最后一个选项必须是NULL(本质就是命令行参数)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id == 0) // 子进程
{
execl("/usr/bin/ls", "ls", "-l", "-a", "--color", NULL);
exit(1);
}
else if (id > 0) // 父进程
{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
printf("father process wait success! child exit code:%d\n", WEXITSTATUS(status));
}
else
{
printf("father process wait failed!\n");
}
}
else
{
printf("fork failed!\n");
}
}
(2)execlp函数
int execlp(const char *file, const char *arg, ...);
execlp函数和execl函数只有第一个参数不同,file参数表示只需要传文件名,不需要传路径,系统会自动在环境变量PATH保存的路径中查找文件
execl("ls", "ls", "-l", "-a", "--color", NULL);
(3)execle函数
int execle(const char *path, const char *arg, ...,char *const envp[]);
execle函数比execl函数多了第三个参数,参数envp用于接收环境变量(可以是自定义的环境变量,也可以是bash父进程的环境变量)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id == 0) // 子进程
{
//传入自定义环境变量
char* const envp[] = {(char*)"HAHA=111", (char*)"HEHE=222", NULL};
execle("/home/zz/240927/test1cpp.exe", "test1cpp.exe", NULL, envp);
//传入父进程的环境变量
// extern char** environ;
// execle("/home/zz/240927/test1cpp.exe", "test1cpp.exe", NULL, environ);
}
else if (id > 0) // 父进程
{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid == -1)
printf("wait failed\n");
else
{
if (WIFEXITED(status))
printf("wait success! child exit code:%d\n", WEXITSTATUS(status));
else
printf("wait success! child abnormal exit\n");
}
}
else
{
printf("fork failed\n");
exit(-1);
}
return 0;
}
(4) execv函数
int execv(const char *path, char *const argv[]);
execv中的 v 表示容器,path是参数路径,argv是将命令选项都放入其中
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id == 0) // 子进程
{
char* const argv[] = {
(char*) "ls",
(char*) "-l",
(char*) "-a",
(char*) "--color",
NULL
};
execv("/usr/bin/ls", argv);
exit(1);
}
else if (id > 0) // 父进程
{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
printf("father process wait success! child exit code:%d\n", WEXITSTATUS(status));
}
else
{
printf("father process wait failed!\n");
}
}
else
{
printf("fork failed!\n");
}
}
(5)execvp函数
int execvp(const char *file, char *const argv[]);
同execlp函数,不需要传路径,只要传文件名即可
char* const argv[] = {
(char*) "ls",
(char*) "-l",
(char*) "-a",
(char*) "--color",
NULL
};
execv("ls", argv);
(6)execve函数
int execve(const char *path, char *const argv[], char *const envp[]);
execve函数比execv函数多了第三个参数envp,用于接收环境变量
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id == 0) // 子进程
{
char* const argv[] = {(char*)"test1cpp.exe", (char*)"-a", (char*)"-b", (char*)"-c", NULL};
char* const envp[] = {(char*)"HAHA=111", (char*)"HEHE=222", NULL};
execve("/home/zz/240927/test1cpp.exe", argv, envp);
// extern char** environ;
// execle("/home/zz/240927/test1cpp.exe", "test1cpp.exe", NULL, environ);
}
else if (id > 0) // 父进程
{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid == -1)
printf("wait failed\n");
else
{
if (WIFEXITED(status))
printf("wait success! child exit code:%d\n", WEXITSTATUS(status));
else
printf("wait success! child abnormal exit\n");
}
}
else
{
printf("fork failed\n");
exit(-1);
}
return 0;
}
(7)execvpe函数
int execve(const char *file, char *const argv[], char *const envp[]);
根据上述所有函数,execvpe函数很容易能理解,此处不做代码演示
补充:
execve既是一个系统调用,也是一个C语言库函数,其他的所有函数都是基于execve封装实现的