创建进程
fork创建进程
fork函数
返回值:子进程中返回0,父进程返回子进程id,出错返回-1。
进程调用fork,当控制转移到内核中,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程的部分数据结构内容拷贝给子进程
- 将子进程添加到系统进程列表当中
- fork返回,调度器开始调度
c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("before fork pid is :%d\n",getpid());
pid_t id = fork();
if(id < 0){
perror("fork");
}
printf("after fork pid id :%d\n",getpid());
printf("fork return value is :%d\n",id);
return 0;
}
上面程序运行结果
可以看出,12行和14行中的打印信息被执行了两次,而且两次的值也不一样。 因为fork之前父进程单独执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器 决定。
fork返回值
- 子进程返回0
- 父进程返回的是子进程的id
写诗拷贝
fork之后,父子进程共享代码,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子 进程来处理请求
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
进程终止
进程退出的场景:
- 代码跑完,结果正确。
- 代码跑完,结果不正确
- 代码没跑完,进程出异常
进程正常终止
- main返回
- 调用_exit
- 使用exit
再平时写C/C++程序时,一般都i会这样写
c
#include <stdio.h>
int main()
{
//...
return 0;
}
都会在main函数最后一行写一句return 0;
return 0;就是进程的退出码,一般0表示成功,非0 表示失败。 一但程序执行失败,需要知道原因,用不同的数字来表示不同的失败原因。
查看进程退出码
使用echo $?
可以查看最近一次进程的退出码
比如系统中没有lll
命令,执行,然后查看进程退出码,可以看到退出码为127
在C标准库函数中,有一个strerror函数,用于将错误码(通常是由系统调用或库函数返回的错误码)转换为对应的错误消息字符串。
c
#include <stdio.h>
#include <string.h>
int main()
{
for (size_t i = 0; i < 255; i++)
{
printf("error code %d:%s\n",i,strerror(i));
}
return 0;
}
Linux下错误码一共有133个。
_exit函数
参数:status 定义了进程的终止状态,父进程通过wait来获取该值 _exit在程序的任意位置都可以终止进程
c
#include <stdio.h>
#include <unistd.h>
void test()
{
printf("Hello World\n");
_exit(1);
}
int main()
{
test();
return 0;
}
运行结果:
main函数中的return 0;还没有执行到进程就终止了。
exit函数
exit和_exit 的区别,exit是c语言的进程终止的函数,而_exit是Linux系统调用接口的函数,c语言在实现exit函数时会封装_exit。
c
#include <stdio.h>
#include <stdlib.h>
void test()
{
printf("Hello World\n");
exit(1);
}
int main()
{
test();
return 0;
}
进程退出码和上面一样,都是1,main函数中的return 0;不会执行,进程就终止了。
exit 和 _exit 的区别
- eixt会刷新缓冲区 更推荐
- _eixt 不会刷新缓冲区。
- exit封装了_exit;
同样的代码,分别使用eixt 和 _exit终止进程,exit会刷新缓冲区。(printf语句没有加\n),而_exit则不会刷新缓冲区。
return退出
- return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返 回值当做 exit的参数。
- 只有在main函数中return才能起到进程终止的作用,在其他函数中return不会终止进程,仅仅只是函数调用结束。
进程等待
进程等待的必要性:
- 子进程退出,父进程不等待,子进程可能变僵尸,从而造成内存泄漏
- 一个进程一旦变为僵尸,谁都无能为力,kill也杀不掉,因为,无法杀掉一个已经死掉的进程。
- 父进程创建子进程,子进程的任务完成的情况,我们需要知道。子进程的运行结果是否正确,是否正常退出(比如ls命令 是bash的子进程,ls的执行情况是需要我们知道的)
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。(父进程必须要做的)
进程等待的方式
wait方法(系统调用)
man 2 wait
认识wait
返回值:
等待成功,返回子进程的pid
等待失败,返回-1。
函数参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
wait:它等待任意子进程退出,而不需要指定特定的子进程ID。如果没有子进程退出,wait
会阻塞当前进程直到有子进程退出。(阻塞等待简单理解就是父进程啥也不干,就只等子进程退出进行回收)
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 )
{
perror("fork\n");
}
else if(id == 0){
//子进程
printf("i am child process.pid = %d\n",getpid());
//子进程退出 退出码为1
exit(1);
}
else{
//父进程
printf("i am Parent process waiting...\n");
sleep(3);//父进程等待三秒后回收子进程
int status = 0;
pid_t waitid = wait(&status);//不想获取子进程退出码可以设置为NULL
//等待失败处理
if(waitid == -1)
{
perror("wait fail\n");
exit(-1);
}
printf("wait sucess, child excit code = %d,waitid = %d\n",status,waitid);
}
return 0;
}
运行结果:
这里父进程获取到的子进程退出码并不是1,而是256 这是因为status占4个字节,32个比特位。前十六位不用关心,后是6位前8位表示进程正常的退出码,最后七位表示进程异常收到的信号,退出码和信号中间的一位是core dump标志位,和信号有关,这里不用深究。
exit(1) 则是进程正常退出,也没有收到异常信号,status则是
这就是256的原因。 如果想直接拿到进程的退出码,而不是status,(status>>8)& 0xff
即可 上面代码31行修改为
c
printf("wait sucess, child excit code = %d,waitid = %d\n",(status>>8)&0xFF,waitid);
运行结果:
waitpid方法(系统调用)
等待指定进程或者任意子进程,相比wait更灵活 man 2 waitpid
认识waitpid
参数:
- pdi:等待子进程的id,若设置为-1,则和wait等效
- status:输出型参数,获取子进程的退出状态,不关心子进程退出状态设置为NULL
- options:设置等待方式,阻塞等待或者非阻塞等待
返回值:
- 等待成功返回被等待进程的pid。
- 等待方式设置为非阻塞等待(WNOHANG),而调用中waitpid发现没有已退出的子进程可收集,则返回0;
阻塞等待示例:
c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
//waitpid阻塞等待
pid_t id = fork();
if(id < 0 ){
perror("fork\n");
exit(-1);
}
else if(id == 0){
//子进程
printf("i am child process. pid = %d\n",getpid());
sleep(3);
exit(1);
}
else {
//父进程
printf("parent process waiting ...\n");
int status = 0 ;
pid_t ret = waitpid(-1,&status,0);//阻塞等待任意子进程
if(WIFEXITED(status) && ret == id){//等待成功
printf("wait sucess,child return code = %d,ret = %d\n",WIFEXITED(status),ret);
}
else{//等待失败
printf("wait fail\n");
}
}
return 0;
}
运行结果:
等待成功,子进程退出码为1,waitpid返回值为子进程的pid
WEXITSTATUS 和 WIFEXITED
WIFEXITED
和 WEXITSTATUS
是在 <sys/wait.h>
头文件中定义的两个宏,用于处理子进程的退出状态信息。
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
非阻塞等待示例:
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
//waitpid非阻塞等待
pid_t id = fork();
if(id < 0 ){//失败处理
perror("fork\n");
exit(-1);
}
else if(id == 0){//子进程
printf("i am child process. pid = %d\n",getpid());
sleep(3);
exit(1);
}
else {//父进程
printf("parent process waiting ...\n");
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1,&status,WNOHANG);//非阻塞等待任意子进程
if(0 == ret){//子进程还没退出,父进程非阻塞等待可以做其他事情
printf("child process is running,parent do other things\n");
sleep(1);
}
}while(0 == ret);
//等待成功
if(WIFEXITED(status) && ret == id){
//打印子进程退出码和waitpid返回值
printf("wait sucess,child return code = %d,ret = %d\n",WEXITSTATUS(status),ret);
}
//等待失败
else{
printf("wait fail\n");
}
}
return 0;
}
运行结果:
父进程非阻塞等待子进程时还可以做其他的事情, wiatpid返回值时子进程的id
阻塞等待和非阻塞等待
阻塞等待:
阻塞等待会导致进程无法执行其他任务,直到等待的事件发生。 waitpid函数设置为阻塞等待,只需将options参数设置为0
-
阻塞等待的优点:
- 实现简单,易于理解。
- 不需要额外的代码来检查事件状态。
-
阻塞等待的缺点:
- 整个进程或线程被挂起,无法执行其他任务。
- 可能导致资源浪费,因为进程被阻塞时,它可能无法充分利用系统资源
非阻塞等待:
waitpid函数设置为阻塞等待,需要将options参数设置为WNOHANG。(一个宏)
-
非阻塞等待的优点:
- 可以充分利用系统资源,因为在等待事件的同时可以执行其他任务。
- 更灵活,适用于需要同时处理多个任务的情况。
-
非阻塞等待的缺点:
- 实现较为复杂,需要额外的代码来轮询或处理事件通知。
- 可能会增加系统负载,因为需要周期性地检查事件状态
进程程序替换
进程程序替换是指一个正在运行的进程将自己的地址空间、代码、数据和堆栈等信息替换为另一个程序的内容。
- 用fork创建的子进程和父进程执行的是相同的程序,但有可能执行不同的代码分支。一般fork创建的子进程需要调用exec函数来执行另一个程序。
- 当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
- 调用exec并不会创建新的子进程,所以调用exec前后该进程的id并未改变。
- 程序替换是通过特定的接口,加载到磁盘上的一个程序,加载到调用进程的地址空间中
替换函数
有六种以exec开头的函数,统称exec函数:
c
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 execvpe(const char *file, char *const argv[], char *const envp[]);
execl函数
cpp
int execl(const char *path, const char *arg, ...);
- 参数1:
path
是要执行的程序的路径(需要指定路径)。 - 参数2:是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。 示例:
cpp
#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){
perror("fork fail\n");
}
else if(id == 0){
//子进程
printf("i am child process, pid = %d",getpid());
//子进程执行ls -a 命令
execl("/usr/bin/ls","ls","-a",NULL);
printf("exec end\n");
exit(1);
}
else{
//父进程
int status = 0;
pid_t ret = waitpid(id,&status,0);//阻塞等待指定子进程
if(WIFEXITED(status) && ret == id)//等待成功
{
printf("wait sucess,child return code = %d,ret = %d, id = %d\n",WEXITSTATUS(status),ret,id);
}
}
return 0;
}
运行结果:
可以发现 当进程程序替换完成后,exec后面的代码将不再执行。 一旦exec替换失败,才会只执行后面的代码 比如将上面17行要替换的进程改为一个不存在的,
cpp
execl("/usr/bin/lsl","ls","-l",NULL);
执行结果:子进程后面的代码被执行了。
execlp函数
cpp
int execlp(const char *file, const char *arg, ...);
- 参数1:需要执行的程序名称。只需要指定程序名称,不需要指定路径
- 参数2:是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。 示例:子进程执行ls命令
c
execlp("ls","ls","-l",NULL);
execle函数
c
int execle(const char *path, const char *arg, ..., char * const envp[]);
参数1:需要执行程序的路径,
参数2:是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。
参数3:envp:是一个以 NULL
结尾的字符串数组,用于设置新程序的环境变量。
c
// 设置新程序的环境变量
char *envp[] = {"MY_VARIABLE=value", NULL};
// 使用 execle 函数替换当前进程的程序
execle("/bin/echo", "echo", "Hello, execle!", (char *)NULL, envp);
运行结果:
execv函数
c
int execv(const char *path, char *const argv[]);
- 参数1:
path
是要执行的程序的路径。 - 参数2:
argv
是一个以NULL
结尾的指针数组,其中包含新程序的名称和参数。
c
char *const argv[] = {"ls","-l",NULL};
execv("/usr/bin/ls",argv);
运行结果:
execvp函数
c
int execvp(const char *file, char *const argv[]);
不用指定路径即可。
execvpe函数
c
int execvpe(const char *file, char *const argv[], char *const envp[]);
第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾,第三个参数是你自己设置的环境变量。
exec系列函数
- 这些函数如果调用成功,则加载指定的程序并从启动代码开始执行,不再返回。
- 如果调用出错,则返回-1。
- exec函数只有成功的返回值,没有失败的返回值。