linux中,fork()函数非常重要,它从当前进程中创建一个新的进程。新进程为子进程,原始进程为父进程。
#include
pit_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1
当一个进程调用fork之后,就有两个二进制代码相同的进程,自然也会被return两次。
具体的参考大佬的博客:进程控制
fork之前父进程独立执行,fork之后,父子两个执行流分别执行。但谁先执行完全由调度器决定。
通常,父子代码共享。
1)修改内容之前,父子进程的数据段+代码段默认共享;
2)修改内容之后,谁写入,谁发生写时拷贝,即写入的数据开辟新的空间。
退出码0代表进程运行结果正确
退出码非0代表运行结果错误
可以通过echo $?
查看最近一个进程的退出码
1.从main返回
2.调用exit
3.调用_exit
#include
void exit(int status);
status:status 定义了进程的终止状态,父进程通过wait来获取该值
函数功能:终止进程
#include
void _exit(int status);
status:status 定义了进程的终止状态,父进程通过wait来获取该值
函数功能:终止进程
Ctrl C 终止进程
解决僵尸进程问题。父进程通过进程等待的方式,回收子进程资源,获取子进程信息。
进程的退出信息是存放在子进程的 task_struct 中的,所以进程等待的本质就是从子进程 task_struct 中读取退出信息,然后保存到相应变量中去。
#include
#include
pid_t wait(int *status);
返回值:成功返回被等待进程的pid,失败返回-1;
status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL;
开始时父子进程都处于休眠状态,之后子进程运行5s退出,此时由于父进程还要休眠5s,所以没有对子进程进行进程等待,所以子进程变成僵尸状态Z。5s过后,父进程使用 wait 系统调用对子进程进行进程等待,所以子进程由僵尸状态变为彻底死亡状态。
#include
#include
pid_t waitpid(pid_t pid, int *status, int options);
返回值:成功返回被等待进程的pid,失败返回-1;
pid:pid=-1,等待任意一个子进程,与wait等效;pid>0.等待其进程id与pid相等的子进程
status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL;
options:等待方式,options=0,阻塞等待;options=WNOHANG,非阻塞等待
waitpid 可以传递 id 来指定等待特定的子进程,也可以指定 options 来指明等待方式。
printf("exit signal:%d, exit code:%d \n", (status & 0x7f), (status >> 8 & 0xff));
其中,status 按位与上 0x7f 表示保留低七位,其余九位全部置为0,从而得到退出信号;
status 右移8位得到退出状态,再按位与上 0xff 是为了防止右移时高位补1的情况;
Linux 提供了 WIFEXITED 和 WEXITSTATUS 宏 来帮助我们获取 status 中的退出状态和退出信号
该部分参考自:添加链接描述
阻塞式等待即当父进程执行到 waitpid 函数时,如果子进程还没有退出,父进程就只能阻塞在 waitpid 函数,直到子进程退出,父进程通过 waitpid 读取退出信息后才能接着执行后面的语句;
而非阻塞式等待则不同,当父进程执行到 waitpid 函数时,如果子进程未退出,父进程会直接读取子进程的状态并返回,然后接着执行后面的语句,不会等待子进程退出。
轮询
轮询是指父进程在非阻塞式状态的前提下,以循环方式不断的对子进程进行进程等待,直到子进程退出。
原文链接:https://blog.csdn.net/m0_62391199/article/details/128033352
进程程序替换是指父进程用 fork 创建子进程后,子进程通过调用 exec 系列函数来执行另一个程序;当进程调用某一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,然后从新程序的启动例程开始执行;
但是原进程的 task_struct 和 mm_struct 以及进程 id 都不会改变,页表可能会变;所以调用 exec 并不会创建新进程,而是让原进程去执行另外一个程序的代码和数据。
程序替换是在当前进程pcb并不退出的情况下,替换当前进程正在运行的程序为新的程序,即
1、程序替换成功后,运行完新程序,依然会运行原有的代码
2、程序替换成功后,原进程没有退出,使用原进程运行新程序
实现进程程序替换的系统调用函数就一个:execve,其他一系列的 exec 库函数都是为了满足不同的替换场景而对 execve 系统调用进行的封装;主要认识六个 exec 库函数。
#include `
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[]);
这些函数调用成功则加载新的程序并启动代码开始执行,不再返回;如果调用出错则返回-1。
这些 函数一旦调用成功,就代表着原程序的代码和数据已经被新程序替换掉了,原程序后续的语句都不会再被执行了,所以 exec 调用成功后没有返回值,因为该返回值没有机会被使用;只有 exec 调用失败,原程序可以继续往下执行时,exec 返回值才会被使用。
执行程序就两个步骤 :
一是找到该可执行程序;二是指定程序执行的方式。
对于 exec 函数来说,“p” 和非 “P” 用来找到程序,“l” “v” 用来指定程序执行方式;“e” 用来指定环境变量。
(1)execl && execlp
以ls
指令为例
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
execlp("ls", "ls", "-a", "-l", NULL); //函数带有 “p”,可以不带路径参数
可以看到,exec无非就两个参数:
第一个参数是路径参数。注意带 “p” 的 exec 函数可以不带路径的前提是被替换程序处于PATH环境变量中。
第二个参数是如何执行程序。即Linux 命令行中该程序如何执行就如何传参,要注意的是,exec 中需要对不同选项进行分割,即每一个选项都单独分为一个字符串,并且最后一个可变参数设置为 NULL,表示传参完毕。
进程程序替换时如果想要让不同类型文件表现为不同颜色的话,需要显示传递 “–color=auto” 选项。
实例如下:
#include
#include
#include
#include
#include
int main()
{
pid_t pid=fork();
if(pid==-1)
{
perror("fork"),exit(-1);
}
else if(pid==0)
{
printf("child process is running... pid:%d\n ", getpid());
int ret = execl("/usr/bin/ls", "ls", "-l", "-a", "--color=auto", NULL);
if(ret == -1)
{
printf("process exec fail....\n");
exit(1);
}
printf("child process is done... pid:%d\n ", getpid());
return 0;
}
else{
int status = 0;
pid_t ret = waitpid(pid, &status, 0); //进程等待
if(ret == -1)
{
perror("waitpid");
return 1;
}
else
{
printf("exit single:%d, exit code:%d\n", (status & 0x7f),(status >> 8 & 0xff));
}
}
return 0;
}
可以看到在命令行上使用 “ls -a -l” 和使用进程程序替换得到的结果一致。
(2)execv && execvp
“v” 代表参数采用数组的形式传递 – argv 是一个指针数组,数组里面的每一个元素都是指针,每一个指针都指向一个参数 (字符串),同样最后一个元素指向 NULL,代表参数传递完毕;
(3)execle && execvpe
“e” 代表环境变量 – 和 argv 一样,envp 也是一个指针数组,数组里面的每个元素都是一个指针,指向一个环境变量 (字符串),我们可以显式初始化 envp 来传递我们自定义的环境变量,但是这也代表着我们放弃了系统环境变量