2 exec函数族
使用fork()函数创建的子进程,其中包含的程序代码完全相同,只能根据fork()函数的返回值,执行不同的代码分支。
由exec函数族中的函数,则可以根据指定的文件名或路径,找到可执行文件。
- fork:子进程复制父进程的堆栈段和数据段,子进程一旦开始运行,它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再影响;
- exec:一个进程调用exec类函数,它本身就"死亡"了,系统把代码段替换成新的程序代码,废弃原有数据段和堆栈段,并为新程序分配新数据段与堆栈段。
exec函数族中包含6个函数,分别为:
#include <unistd.h>
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 execve(const char *path, char * const argv[], char * const envp[]);
参数说明:
(1)当参数是path,传入的为路径名;当参数是file,传入的可执行文件名;
(2)可以将exec函数族分为execl和execv两类:
- execl类:函数将以列举的形式传入参数,由于参数列表的长度不定,所以要用哨兵NULL表示列举结束;
- execv类:函数将以参数向量表传递参数,char * argv[]的形式传递文件执行时使用的参数,数组中最后一个参数为NULL;
(3)如果没有参数char * const envp[],则采用默认环境变量;如果有,则用传入的参数替换默认环境变量;
- 在 Linux 系统中,环境变量是用来定义系统运行环境的一些参数,比如每个用户不同的家目录(HOME)、邮件存放位置(MAIL)等
[root@localhost ~]# env
ORBIT_SOCKETDIR=/tmp/orbit-root
HOSTNAME=livecd.centos
GIO_LAUNCHED_DESKTOP_FILE_PID=2065
TERM=xterm
SHELL=/bin/bash
【案例 4】在程序中创建一个子进程,之后使父进程打印自己的pid信息,使子进程通过exec函数簇获取系统命令文件,执行ls命令。
test_exec.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
pid_t tempPid;
tempPid=fork();
if(tempPid == -1){
perror("fork error");
exit(1);
} else if(tempPid > 0) {
printf("parent process:pid=%d\n", getpid());
} else {
printf("child process:pid=%d\n", getpid());
//execl("/bin/ls","-a","-l","test_exec.c",NULL); //①
//execlp("ls","-a","-l","test_exec.c",NULL); //②
char *arg[]={"-a","-l","test_exec.c", NULL}; //③
execvp("ls", arg);
perror("error exec\n");
printf("child process:pid=%d\n", getpid());
} //of if
return 0;
} //of main
【问】:如果execvp(“ls”, arg);执行成功,不再执行19,20对应的语句,请根据execvp的函数特点分析原因。
【答】:一个进程调用exec类函数,它本身就"死亡"了,系统把代码段替换成新的程序代码,废弃原有数据段和堆栈段,并为新程序分配新数据段与堆栈段。
【案例5】在执行文件execl.c过程中调用execl,从而执行echoarg.c。
文件echoarg.c
#include <stdio.h>
int main(int argc,char *argv[]) {
int i = 0;
for(i = 0; i < argc; i++) {
printf("argv[%d]: %s\n",i,argv[i]);
}//of if
return 0;
}//of main
编译echoarg.c并运行,结果如下:
文件execl.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void){
printf("before execl\n");
//if(execl("./echoarg", "file1","file2",NULL) == -1){
if(execl("./echoarg","./echoarg", "file1","file2",NULL) == -1){
printf("execl failed!\n");
perror("why");
}//of if
printf("after execl\n");
return 0;
}//of main
编译execl.c并运行,结果如下:
程序分析:
- 若调用execl失败,返回-1,然后执行 printf(“after execl\n”);(打印"after execl");
- 若成功则不执行 printf(“after execl\n”);(不打印"after execl")。
【实例6】获取日期和时间
Linux指令date 可以用来显示或设定系统的日期与时间:
调用execl函数来获取日期和时间:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void){
printf("before execl\n");
if(execl("/bin/date","date",NULL,NULL) == -1){
printf("execl failed!\n");
perror("why");
}//of if
printf("after execl\n");
return 0;
}//of main
编译运行结果如下:
3 进程退出
#include <stdlib.h>
void exit(int status);
参数说明:
(1)status:表示进程的退出状态,0表示正常退出,非0表示异常退出,一般用-1或1表示;
(2)为了可读性,标准C定义了两个宏:EXIT_SUCCESS和EXIT_FAILURE
Linux系统中有一个与exit()函数非常相似的函数:_exit()
#include <unistd.h>
void _exit(int status);
区别:
- _exit:系统会无条件停止操作,终止进程并清除进程所用内存空间及进程在内核中的各种数据结构;
- exit:对_exit进行了包装,在调用_exit()之前先检查文件的打开情况,将缓冲区中的内容写回文件。相对来说exit比_exit更为安全
【案例7】
#include <stdio.h>
#include <stdlib.h>
int main(int argc, const char *argv[]){
/* 标准输出流stdout是行缓冲,遇到“\n”换行符时才实际写入终端。 */
printf("Using exit...\n");
/* 下一个输出不输出换行符 */
printf("This is the content in buffer.");
exit(0); /* 调用exit函数测试 */
printf("After the exit...\n");
return 0;
}//of main
4 特殊进程
- 孤儿进程:父进程负责回收子进程,如果父进程在子进程退出之前退出,子进程就会变成孤儿进程,此时init进程将代替父进程完成子进程的回收工作;
- 僵尸进程:调用exit函数后,该进程不会马上消失,而是留下一个称为僵尸进程的数据结构。它几乎放弃进程退出前占用的所有内存,既没有可执行代码也不能被调度,只是在进程列表中保留一个位置,记载进程的退出状态等信息供父进程回收。若父进程没有回收子进程的代码,子进程将会一直处于僵尸态。