#include
int main()
{
cout << "process: PID->" << getpid() <<", PPID->"<<getppid() << endl;
pid_t id = fork();
if(id == -1)
{
perror("fork fail");
}
cout << "process: PID->" << getpid() <<", PPID->"<<getppid() << endl;
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9XpS1Pjv-1690035968623)(https://raw.githubusercontent.com/roargod/typora_image/main/img/202307220942222.png)]
我们看到在fork
之后的cout打印了两次,而且第二次的PPID是父进程的PID。这说明我们使用fork成功创建了一个子进程。
我们刚刚还提到了fork
函数会有两个返回值,一个返回0, 另一个返回子进程的PID。但是为什么呢?
我们在上一个文章中提到过,在调用fork
函数时后,fork会进行创建进程PCB、mm_struct、页表等操作。在这些操作完成后进程就创建完成了,就会return进行返回。当父子进程都return后,就会有两个返回值。
一个父进程可以有多个子进程,每个子进程都只有一个父进程,给父进程返回子进程自己的PID可以方便父进程给子进程发派任务,这就是不同返回值的原因。
当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。
我们在上一篇文章中具体谈到了,有兴趣可以去看一看。
进程退出一般分为三种种:
我们可以通过以下命令来查看Linux程序执行的结果:
[---@VM-8-4-centos day02]$ echo $?
0就说明我们刚才的代码是执行成功的,正常退出的。(我们没有进行设定)
我们可以通过一段代码来查看各种返回值对应的错误原因。
#include
#include
#include
#include
using namespace std;
int main()
{
for(int i = 0; i < 100; i++)
{
printf("%d : %s\n", i, strerror(i));
}
return 0;
}
例如:在main函数最后退出。
例如:代码执行一半进行退出。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HEohpmxg-1690035968624)(https://raw.githubusercontent.com/roargod/typora_image/main/img/202307221033083.png)]
而且我们可以定义退出时的状态,父进程还可以通过wait来获取这个值。
_exit
同样可以在代码的任何位置退出进程,但它只会直接终止进程,而进程中的部分代码不会被执行。上述的部分代码不会被执行,例如:
void test()
{
cout << "hello world" ;
_exit(1);//#include
}
int main()
{
test();
return 0;
}
要是我们不强制刷新缓冲区,hello world就不会被打印出来。
return只有在主函数中才能退出进程,exit和_exit可以在任何地方退出进程。
exit和return在结束进程的时候会执行用户的清除函数 刷新缓冲区 关闭流等 而_exit则会直接退出。
exit是对_exit的封装,_exit
是系统调用。使用exit函数退出进程前,exit函数会先执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再调用_exit
函数终止进程。
执行return num等于执行exit(num)。
向进程发kill信号等退出,导致程序异常。
代码错误导致进程运行时异常退出,如除0、空指针等。
两种方法:
pid_t wait (int* status)
作用:等待任意子进程。
status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
返回值:成功返回对应子进程的PID,失败返回-1
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
pid_t id = fork();
if (id > 0)
{
sleep(10);
int status = 0;
pid_t ret = wait(&status);
if (ret == -1)
{
perror("wait fail");
exit(1);
}
cout << "wait success" << endl;
cout << "exit code-> " << status << endl;
}
else if (id == 0)
{
int count = 5;
while (count--)
{
cout << "我是子进程, PID->" << getpid() << ", PPID->" << getppid() << endl;
sleep(1);
}
cout << "等待回收" << endl;
exit(1);
}
else
{
perror("fork fail");
}
return 0;
}
pid_t waitpid(pid_t pid, int* status, int options)
作用:等待指定子进程或任意子进程
pid:指定等待子进程的PID,设置为-1即为等待任意子进程
status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
options:设置为0时阻塞等待,设置为WNOHANG,非阻塞等待。
阻塞等待:当父进程执行到 waitpid 函数时,如果子进程还没有退出,父进程就只能阻塞在 waitpid 函数,直到子进程退出,父进程通过 waitpid 读取退出信息后才能接着执行后面的语句。例如,你约你的女朋友出去吃饭,让她化好妆打电话叫你来接她,从你开始等她到她花好妆打电话给你,这段时间你什么都不做,这就是阻塞等待。
非阻塞等待:当父进程执行到 waitpid 函数时,如果子进程未退出,父进程会直接读取子进程的状态并返回,然后接着执行后面的语句,不会等待子进程退出。例如,你约你女朋友出去吃饭,让她化好妆打电话叫你来接她,从你开始等她到她给你说好了,这段时间你每2分钟打电话问她好了没有(当然不建议大家这么做,哈哈)。这就是非阻塞等待。
我们使用一下阻塞等待加深一下理解:
int main()
{
pid_t id = fork();
if (id > 0)
{
cout << "我是父进程,我已经开始等待了" << endl;
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
if (ret == -1)
{
perror("wait fail");
exit(1);
}
cout << "wait success" << endl;
cout << "exit code-> " << status << endl;
}
else if (id == 0)
{
int count = 5;
while (count--)
{
cout << "我是子进程, PID->" << getpid() << ", PPID->" << getppid() << endl;
sleep(1);
}
cout << "等待回收" << endl;
exit(1);
}
else
{
perror("fork fail");
}
return 0;
}
父进程没有直接打印wait success
,而是获取到子进程退出的状态值后才运行waitpid之后的代码。
status虽然是一个整型变量,但不能当作整型变量来看。
我们可以把它看作一个位图。(仅仅看低16位比特)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JZYESesH-1690035968625)(https://raw.githubusercontent.com/roargod/typora_image/main/img/202307221836635.png)]
原理图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RxIpdqqN-1690035968626)(https://raw.githubusercontent.com/roargod/typora_image/main/img/202307221841483.png)]
我们可以通过操作符得到退出码和退出信号。
exit_code = (status >> 8) & 0xFF//退出码
exit_signal = status & 0x7F//退出信号
当然贴心的系统当中也提供了两个宏来获取退出码和退出信号。
exit_code = WEXITSTATUS(status);//获取退出码
ret = WIFEXITED(status);//是否正常退出
PS:如果当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了。
在我们使用fork
创建出子进程后,有两种情况:一是子进程执行父进程的部分代码,二是子进程和父进程执行不同的代码。
我们说的第二种情况就是进程程序替换。
当我们使用fork创建子进程后,再使用exec*
函数,使得该进程的用户空间代码和数据完全被新程序所替换,并执行新的程序。
我们之前说过进程创建时会同时创建PCB、mm_struct和页表等,在程序替换后这些都统统没有发生改变,只是物理内存中的数据和代码发生了改变。当子进程发生程序替换时还会发生我们之前提到的写时拷贝,使得父子进程间互不干扰。
第一个参数是我们执行的路径。
另一个参数是可变参数列表,我们需要进行的操作,最终以NULL结尾。
如:
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
第一个参数是我们执行的文件名。
另一个参数是可变参数列表,我们需要进行的操作,最终以NULL结尾。
execlp("ls", "ls", "-a", "-l", NULL);
第一个参数是我们执行的路径。
第二个参数是可变参数列表,我们需要进行的操作,最终以NULL结尾。
第三个参数是我们设定的环境变量。
char* myenvp[] = { (char*)"val=100", NULL };
execle("./mycode", "./mycode", NULL, myenvp);
第一个参数是我们执行的路径。
第二个参数是指针数组,表示我们需要进行的操作,以NULL结尾。
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);
第一个参数是我们执行的文件名。
第二个是指针数组,表示我们需要进行的操作,以NULL结尾。
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
第一个参数是我们执行的路径。
第二个参数是指针数组,表示我们需要进行的操作,以NULL结尾。
第三个参数是我们设定的环境变量。
char* myargv[] = { "./mycode", NULL };
char* myenvp[] = { "val=100", NULL };
execve("./mycode", myargv, myenvp);
函数名理解:
一个具体的代码理解exe*函数
myexe.cc
#include
#include
using namespace std;
int main()
{
printf("USER: %s\n", getenv("USER"));
printf("PWD: %s\n", getenv("PWD"));
printf("VAL: %s\n", getenv("val"));
return 0;
}
mycode.cc
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
pid_t id = fork();
if (id > 0)
{
int status = 0;
// cout << "我是父进程" << endl;
int ret = waitpid(-1, &status, 0);
if (ret == -1)
{
perror("wait fail");
}
cout << "wait success: "
<< "exit_code-> " << ((status >> 8) & 0xFF) << endl;
}
else if (id == 0)
{
// execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
char *const myenvp[] = {(char*)"val=100", (char*)"PWD=./day02", (char*)"USER=wsj", NULL};
int ret = execle("./myexe", "./myexe", NULL, myenvp);
if(ret == -1) perror("execle fail");
exit(0);
}
else
{
perror("fork fail");
}
return 0;
}
从execle的传参可以看出,我们通过mcode.cc代码来调用myexec.cc进行程序替换。
其他exec*函数,一通百通。