1.exec函数族
exec函数族介绍
exec 函数族的作用是根据指定的文件名(或路径)找到可执行文件,并用它来取代调用进程的内容,即在调用进程内部执行一个可执行文件。
exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样。调用失败时会返回 -1,从原程序的调用点接着往下执行。
exec函数族作用图解
现有一个进程(进程的虚拟地址空间由内核区和用户区组成);
如果在当前进程里调用了了exec函数组中的函数,指定执行a.out
此时并不是重新创建了一个新的进程,而是把原来进程的用户区数据替换成a.out的用户区数据
exec函数族
int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, 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[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
int execl(const char *path, const char *arg, .../* (char *) NULL */);
:
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
参数:
- path:需要指定的执行的文件的路径或者名称
- arg:是执行可执行文件所需要的参数列表
第一个参数一般没有作用,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表
参数最后需要以NULL结束
返回值:
只有当调用失败,才会有返回值,返回-1,并且设置errno
如果调用成功,没有返回值
示例:
int main() {
pid_t pid = fork(); //创建一个子进程,在子进程中执行exec函数族中的函数
if(pid > 0) { //父进程
printf("i am parent process, pid : %d\n",getpid());
sleep(1); //如果不加sleep(1)就会产生孤儿进程
}else if(pid == 0) { //子进程
execl("hello","hello",NULL);
//execl("/bin/ps", "ps", "aux", NULL);
printf("i am child process, pid : %d\n", getpid()); //如果execl执行成功,该行代码不会执行
}
for(int i = 0; i < 3; i++) {
printf("i = %d, pid = %d\n", i, getpid()); //如果execl执行成功,只会打印父进程
}
return 0;
}
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
:
#include <unistd.h>
int execlp(const char *file, const char *arg, ... );
- 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。
参数:
- file:需要执行的可执行文件的文件名
- arg:是执行可执行文件所需要的参数列表
第一个参数一般没有作用,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表
参数最后需要以NULL结束
返回值:
只有当调用失败,才会有返回值,返回-1,并且设置errno
如果调用成功,没有返回值
示例:
int main() {
pid_t pid = fork(); //创建一个子进程,在子进程中执行exec函数族中的函数
if(pid > 0) { //父进程
printf("i am parent process, pid : %d\n",getpid());
sleep(1); //如果不加sleep(1)就会产生孤儿进程
}else if(pid == 0) { //子进程
execlp("ps", "ps", "aux", NULL); //有时不需要指定绝对路径,会到环境变量中查找指定的可执行文件
printf("i am child process, pid : %d\n", getpid()); //如果execl执行成功,该行代码不会执行
}
for(int i = 0; i < 3; i++) {
printf("i = %d, pid = %d\n", i, getpid()); //如果execl执行成功,只会打印父进程
}
return 0;
}
int execle(const char *path, const char *arg, .../*, (char *) NULL, 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[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
l(list) 参数地址列表,以空指针结尾
v(vector) 存有各参数地址的指针数组的地址
p(path) 按 PATH 环境变量指定的目录搜索可执行文件
e(environment) 存有环境变量字符串地址的指针数组的地址
如:
int execv(const char *path, char *const argv[]);
argv是需要的参数的一个字符串数组
char * argv[] = {"ps", "aux", NULL};
execv("/bin/ps", argv);
int execve(const char *filename, char *const argv[], char *const envp[]);
char * envp[] = {"/home/nowcoder", "/home/bbb", "/home/aaa"};
2.进程控制
进程退出
标准C库中的进程退出函数:
#include <stdlib.h>
void exit(int status);
Linux操作系统的进程退出函数:
#include <unistd.h>
void _exit(int status);
//参数status是进程退出时的一个状态信息。
//父进程回收子进程资源的时候可以获取到。
两种进程退出函数的区别:
示例:
int main() {
printf("hello\n"); //加了换行,在内部printf就会自动刷新IO缓冲区,输出数据
printf("world"); //没有加换行,将数据放入IO缓冲区中但没有刷新
exit(0); //标准C库
//_exit(0); //Linux操作系统
return 0; //执行进程退出成功,该行就不会执行
}
孤儿进程
父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process)。
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init进程会循环地 wait() 已经退出的子进程。
因此孤儿进程并没有什么危害。
示例:
int main() {
pid_t pid = fork(); //创建子进程
// 判断是父进程还是子进程
if(pid > 0) { //当前是父进程
printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
} else if(pid == 0) { //当前是子进程
sleep(1); //延迟一秒,父进程运行结束、子进程才开始运行
printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());
}
for(int i = 0; i < 3; i++) {
printf("i : %d , pid : %d\n", i , getpid());
}
return 0;
}
子进程的父进程ID为1,因为该子进程为孤儿进程,会被内核分配给进程号为1,由ppid=1的进程回收孤儿进程的资源
为什么父进程结束后会显示终端:
当运行可执行程序时会默认切换到后台运行,当有输出时再打印到终端(切换到前台)。
当父进程结束后就切换到前台,因为在当前终端运行的./orphan,当前终端的进程ID为2163,是orphan的父进程,当orphan运行完毕,就切换到了前台。
但此时子进程还没有结束,且还有输出。
僵尸进程
每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉,需要父进程去释放。
进程终止时,父进程尚未回收子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
僵尸进程不能被 kill -9 杀死,这样就会导致一个问题,如果父进程不调用 wait() 或 waitpid() 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害。
进程回收
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。
父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
wait() 和 waitpid() 函数的功能一样,区别在于,wait() 函数会阻塞,waitpid() 可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束。
注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
pid_t wait(int *wstatus);
:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收子进程的资源。
参数:int *wstatus
进程退出时的状态信息,传入的是一个int类型的地址
返回值:
- 成功:返回被回收的子进程的id
- 失败:-1 (所有的子进程都结束 或 调用函数失败)
调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1
退出信息相关宏函数:
WIFEXITED(status) //如是是非0,表示进程正常退出
WEXITSTATUS(status) //如果上宏为真,获取进程退出的状态(exit的参数)
WIFSIGNALED(status) //如果是非0,表示进程异常终止
WTERMSIG(status) //如果上宏为真,获取使进程终止的信号编号
WIFSTOPPED(status) //如果是非0,表示进程处于暂停状态
WSTOPSIG(status) //如果上宏为真,获取使进程暂停的信号的编号
WIFCONTINUED(status) //如果是非0,表示进程暂停后已经继续运行
示例:
int main() {
pid_t pid; //有一个父进程,创建5个子进程(兄弟)
for(int i = 0; i < 5; i++) { //创建5个子进程
pid = fork();
if(pid == 0) { //防止子进程产生子进程
break;
}
}
if(pid > 0) { //父进程
while(1) {
printf("parent, pid = %d\n", getpid());
// int ret = wait(NULL); //如果传递NULL,表示不需要获得子进程退出的状态
int st;
int ret = wait(&st);
if(ret == -1) { //没有子进程可以回收
break;
}
if(WIFEXITED(st)) { //是不是正常退出
printf("退出的状态码:%d\n", WEXITSTATUS(st));
}
if(WIFSIGNALED(st)) { //是不是异常终止
printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
}
printf("child die, pid = %d\n", ret);
sleep(1);
}
} else if (pid == 0){ //子进程
printf("child, pid = %d\n",getpid());
sleep(1);
exit(0);
}
return 0; // exit(0)
}
pid_t waitpid(pid_t pid, int *wstatus, int options);
:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:回收指定进程号的子进程,可以设置是否阻塞
参数:
- pid:
pid > 0 : 某个子进程的pid
pid = 0 : 回收当前进程组的所有子进程
pid = -1 : 回收所有的子进程,相当于 wait() (最常用)
pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程
- options:设置阻塞或者非阻塞
0 : 阻塞
WNOHANG : 非阻塞
返回值:
> 0 : 返回子进程的id
= 0 : 在options=WNOHANG情况下才会返回0, 表示还有子进程
= -1 :错误,或者没有子进程了