进程创建
使用fork函数可以在一个进程中创建一个子进程
fork函数
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("begin: 我是一个进程,pid: %d,ppid:%d\n",getpid(),getppid());
pid_t id = fork();
if(id == 0)
{
//子进程
printf("我是父进程,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
else if(id >0)
{
//父进程
printf("我是父进程,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
else
{
//error
}
//printf("after fork\n");
return 0;
}
为什么这个代码可以同时满足if条件和else if条件?
在这段代码中,fork()调用之后就在原本的process进程中多创建了一个子进程。而在之前的学习中我们知道进程是由PCB+代码和数据,而学了地址空间之后,我们知道所谓的代码和数据其实是进程地址空间结构体+页表,也很好解释了我们的代码为什么可以执行if也可以执行else if的语句,以及这里的id为什么可以一个变量存两个值(fork函数返回值:给父进程返回子进程pid,给子进程返回0)。
是因为在fork过后,我们创建了一个子进程,代码是与父进程共用的,数据在没有修改之前和父进程是共用的,但是如果子进程对数据进行了修改就会在页表重新映射一块内存给子进程存修改后的值,但是虚拟地址(我们C语言%p打印出来的值还是一样的)。这就叫写时拷贝
所以,在父进程代码中的pid变量大于0,所以父进程执行了大于0的条件,在子进程代码中的pid变量等于0,所以子进程执行了等于0的条件。
注意:父子进程,兄弟进程被创建出来,谁先运行,我们是不能确定的,这由调度器决定
fork常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子
进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
系统中有太多的进程
实际用户的进程数超过了限制
进程终止
对于进程来说,终止只会有三个情况:
- 进程正常终止,结果正确
- 进程正常终止,结果不正确
- 进程异常终止
对于一个进程来说,创建出来就是为了完成工作的,而终止的结果恰恰就是那个工作汇报,给创建进程的那个“人”汇报。
退出码和错误码
为什么main函数总是会返回return 0? 1? 2?, 这个东西给谁了?为什么要返回这个值?
echo $?表示最近一个程序执行后的退出码
从上面那个例子来说,
我process_stop这个进程返回0给到我们的bash父进程,证明我的main返回的0是个父进程的。而0是退出码,表示进程的运行结果是否正确。0表示正确。
为什么要return 0 ,我们直接用printf
打印出来不好吗?但是对于一些用于网络通行或者其他不能打印的场景来说,退出码是辨别进程能否正确执行的标准
错误码:
错误码通常是由系统调用返回的,用于指示系统调用是否成功执行。如果系统调用成功,通常返回 0。如果调用失败,则返回一个负值,这个负值代表一个错误码,可以通过 errno 全局变量获取具体的错误信息。错误码是由操作系统定义的,不同的错误码对应不同的错误情况。
strerror函数:把错误码转换成为字符描述
退出码:
退出码是程序结束时返回给操作系统或父进程的一个值,用于指示程序是正常结束还是遇到了某种错误。退出码通常是一个非负整数,由程序通过 exit() 函数或返回语句返回。退出码通常遵循一定的约定,例如,返回 0 通常表示程序成功执行,而返回非 0 值表示程序执行过程中遇到了问题。
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
错误码和退出码的区别
用途:错误码用于指示系统调用的状态,而退出码用于指示程序的执行状态。
来源:错误码由系统调用返回,退出码由程序显式返回。
exit函数和 _exit函数
当调用 exit 时,它会执行一些清理工作,包括:
- 关闭所有打开的文件描述符(除非它们被设置为保持打开)。
- 刷新所有打开的文件流(如 stdout、stderr)并将缓冲区的内容写入文件。
- 调用所有注册的退出处理函数(通过 atexit 注册)。
- 发送 SIGCHLD 信号给父进程。
_exit 函数提供了一种立即终止进程的方式,不执行任何清理工作。这意味着:
- 不关闭文件描述符。
- 不刷新文件流。
- 不调用退出处理函数。
- 不发送 SIGCHLD 信号给父进程。
进程等待
在之前进程状态提到过,一个没有被回收资源的子进程退出会让该子进程会变成僵尸进程。僵尸进程的危害特别大,无法用kill -9去杀死他,一直占有内存,导致内存泄露。这时候则需要我们进程等待出来解决这个问题,在父进程调用wait/waitpid,来进行对子进程状态检测和回收。
waitpid函数和wait函数
wait函数一次只能等待一个进程退出
返回值
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数
pid_t pid
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待传参pid的指定进程。
int* wstatus
是一个输出型参数,用于接收子进程的结束状态,这个wstatus并不是一个简单的一个整形,可以看做为一个位图。如图:
现在暂时只关心前16位,这前16位中得到前8位是作为退出码。而后8位作为错误码。
WIFEXITED(wstatus): 一个宏函数,传入wstatus,获取该进程的运行是否异常,若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(wstatus): 一个宏函数,传入wstatus,获取该进程的退出结果,若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
int options
0:阻塞等待,当我们的子进程还没有返回时,我们父进程调用waitpid去等待子进程,这时父进程就会一直等待子进程的返回什么都不做
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID(非阻塞轮询)非阻塞轮询可以让父进程边等待边去做自己事。
进程替换
原理
创建子进程的PCB,进程地址空间和页表,写时拷贝父进程的数据,发生进程替换之后,从磁盘中载入该进程的数据与代码到该进程的物理内存中,再去修改该进程的页表映射(此过程也是写时拷贝)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("before: I am a process,pid:%d,ppid:%d\n",getpid(),getppid());
execl("/usr/bin/ls","ls","-l",NULL);
printf("after: I am a process,pid:%d,ppid:%d\n",getpid(),getppid());
exit(0);
}
int status = 0;
pid_t ret = waitpid(-1,&status, 0);
if(ret > 0)
{
printf("wait success, father pid:%d,ret id:%d\n",getpid(),ret);
}
return 0;
}
从输出结果可以观察得出,进程替换只会把a进程的代码数据替换到b进程中,不会创建新进程,pcb、进程地址空间。程序替换之后,execl函数执行后的代码就不是被执行,因为进程的内容已经被替换进来的进程替换掉了。
替换失败怎么办?
我把路径改成错误路径,后发现,替换失败就会继续执行后面的代码,替换成功就会执行替换后的代码。
注意:exec* 函数,只有失败返回值,没有成功返回值,我们可以通过给进程退出码,父进程等待接收的方式,来判断到底有没有接收成功。
进程替换接口
所有exec的第一个参数,都是要拿来替换的进程路径
l:像list一样传参(结点类型为const char*)
p:会在默认的环境变量路径中寻找进程
v:像vector一样传参(char const*数组)
e:可以传入环境变量给子进程,该环境变量只有子进程和他的子进程拥有。
注意:l和v传参都要以null结尾
使用实例代码:
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("before: I am a process,pid:%d,ppid:%d\n",getpid(),getppid());
char s1[] = "hello=11";
putenv(s1);
printf("子进程环境变量:%s\n",getenv("hello"));
/* 用execl调用ls */
// execl("/usr/bin/lsa","ls","-l",NULL);
/* 用execlp调用ls -a +p会去环境变量搜索进程路径*/
// execlp("ls","ls","-a",NULL);
/* 用execv调用自定义进程*/
// char *const argv[] = {"otherExe","-a","-l",NULL};
// execv("./otherExe",argv);
// printf("after: I am a process,pid:%d,ppid:%d\n",getpid(),getppid());
// exit(0);
/* 用execl执行python代码*/
// execl("/usr/bin/python3","python3","test.py",NULL);
/* 用execle传递环境变量*/
//自定义环境变量数组
char *const myenv[] = {
"MYVAL=11",
NULL
};
execle("./otherExe","otherExe","-a",NULL,myenv);
}
int status = 0;
pid_t ret = waitpid(-1,&status, 0);
printf("父进程环境变量:%s\n",getenv("hello"));
if(ret > 0)
{
printf("wait success, father pid:%d,ret id:%d\n",getpid(),ret);
}
return 0;
}