Linux进程和execX函数簇

来源:互联网 发布:java什么水平月薪一万 编辑:程序博客网 时间:2024/05/16 14:07

1. 进程

  进程可以理解为正在内存中运行的程序的实例。每个进程都有一个进程描述符PID,且每个进程都是由其父进程创建的(init进程除外),所以都有父进程描述符PPID。

pid_t getpid(void);     //获取进程IDpid_t getppid(void);    //获取父进程ID

  对于程序员来说,进程的作用不外乎是用来运行其他程序,即在某个进程中开启另一个进程去运行其他程序。这个”另一个进程”也称为子进程。子进程跟父进程一样,要得以正常运行都需要运行环境,即程序参数、环境变量、运行权限和工作路径

1.1 程序参数

  运行程序命令行的第一个参数,如”./a.out hello.file”中的”./a.out”

1.2 环境变量

  环境变量通过getenv()获取、setenv()、putev()设置、unsetenv()删除

1.2.1 获取环境变量
int main(int argc, char* argv[]){    printf("main: pid = %d, ppid = %d\n", getpid(), getppid());    printf("PAHT = %s\n", getenv("PATH"));      //获取本进程的PATH环境变量    return 0;}

  运行结果:

7_fork_test$ ./a.out main: pid = 3330, ppid = 2872PAHT = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin7_fork_test$

  上面程序是运行在Linux终端的,所以该进程为bash所创建的子进程,故其ppid等于bash的pid。
这里写图片描述
  子进程的环境变量是由父进程赋予的,也就是a.out进程的环境变量是由bash进程给予的。命令env的执行结果是获取当前进程的所有环境变量,getenv(“PATH”)实现的操作则是从这些环境变量中抽取出PATH环境变量部分。
  想要获取该进程(的父进程赋予)的所有环境变量可以将main函数原型声明定义的:

int main(int argc, char* argv[], char *environ[])

  第3个参数environ等于父进程传来的所有的环境变量,打印操作为:

while (*environ != NULL){    printf("%s\n", *environ++);}

  但是这样的main函数的定义并不通用。虽然历史上大多数unix系统对mian函数虽然都提供了如上的三个参数,但是后来ansi c规定main函数只有前面两个参数,因为环境变量environ是一个全局变量,作为main的参数三传进main函数并没有什么益处。
  getenv()和setenv()等函数操作的是特定的环境变量而非environ变量。注意,argv和environ结束条件是都是NULL(这一点很重要,等下会用到)。所以打印运行参数列表还可以:

while(*argv != NULL){    printf("%s\n", *argv);}

  因为带3个参数的main原型不通用,所以操作环境变量的代码为:

#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <unistd.h>extern char **environ;  //外部全局变量,用于存放环境变量,声明在stdlib.h中int main(int argc, char* argv[]){    while (*environ != NULL)    {        printf("%s\n", *environ++);    }    return 0;}
1.2.2 设置环境变量
setenv("PATH", "ABCDEF", 1);putenv("PATH = hello");unsetenv("PATH");

1.3 运行权限

  运行权限通过setuid()、seteuid()和setgid()、setegid()设置。注意这些函数是修改权限操作,所以需要root用户执行。

1.4 工作路径

  工作路径可以通过getcwd()获得,chdir()改变

2. execX函数簇

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[]);

  当进程调用如上execX函数簇的任意一个时来启动新程序时,该进程用户空间(代码段、数据段、bss段、堆栈)完全被新程序覆盖。execX函数簇不会创建新的进程。这些函数的区别在于:
  a. 标志新程序所在位置是使用路径还是文件名;
  b. 参数的传递是使用参数列表还是argv[]数组的数组形式;
  注意,如果是使用文件名来标识新程序的,也就是你没有指定可执行程序的路径,所以它会去$PATH环境变量所记录的路径去搜索该程序。
  a. 函数名中含有’p’表示该函数不需要指定可执行程序的路径,否则需要;
  b. 函数名中含有’l’表示该函数是以参数列表的形式接收形参,否则argvp[]数组形式;

2.1 execl()函数

  execl函数是对新程序的标识需要指定路径,且形参是列表形式的:

int main(int argc, char* argv[]){    execl("/bin/ls", "ls", "-l", "/home/tom", NULL);     return 0;}

  参数1: 指定可执行程序
  参数2开始到最后一个参数之前: 可执行程序的参数列表
  最后一个参数: NULL是参数列表的结束标志

2.2 execlp()函数

  execlp函数对新程序不需要指定路径,它是从$PATH环境变量所指定的目录中查找文件名为第一个参数指示的字符串,找到后执行它。第二个及以后的参数表示执行该文件时传递的参数列表,同理,最后一个参数必须为NULL。

int main(int argc, char* argv[]){    execlp("ls", "ls", "-l", "/home/tom", (char* )0);     return 0;}

2.3 execle()函数

  execle()函数跟execl()函数的差异在于execl()可传入环境变量envp[],也就是说进程启动新程序的时候可以为新程序指定环境变量。(当然,对于其他不能为新程序指定环境变量的函数,默认是把当前进程的环境变量赋给新程序)

//主调进程extern char **environ;int main(int argc, char* argv[]){    execle("./new_pro", "", NULL, environ);     printf("main is return\n");  //这一句不会被执行    return 0;}
// 新程序extern char **environ;int main(void){    int i = 0;    while (environ[i] != NULL)        printf("%s\n", environ[i++]);  //打印出所有环境变量    return 0;}

  若在进程中修改环境变量然后传给新程序,新程序将得到被进程修改后的环境变量;相反,若在新程序中修改环境变量则不会影响父进程的环境变量。

// 主调进程int main(int argc, char* argv[]){    setenv("PATH", "ABCDEF", 1);    execle("./new_pro", "", NULL, environ);     printf("main is return\n");  //这一句不会被执行    return 0;}
// 新进程extern char **environ;int main(void){    printf("PATH = %s\n", getenv("PATH"));    return 0;}

  运行结果:

7_fork_test$ ./a.out PATH = ABCDEF7_fork_test$ 

  execX函数簇用于启动新程序没错,它的特点是不会新创建一个进程,而是会把新程序的进程空间赋给主启动进程进程空间,这么一来,在execX函数调用处将后将不会执行主启动进程的代码了。要想启动一个新的进程来执行新程序,父子进程有独立的进程空间,这就需要创建进程fork()函数了。

2.4 execv()函数

  execv()函数对新程序需要指定路径,形参是以”argv[]”数组形式传进来的

// 主调进程int main(void){    char* argv[] = {"hello", "word", NULL};  //注意argv[]与形参列表的区别,列表还需要传入再可执行程序名称    execv("./new_pro", argv);    return 0;}
// 新进程int main(int argc, char** argv){    while (*argv != NULL)    {        printf("%s\n", *(argv++));    }    return 0;}

  运行结果:

7_fork_test$ ./a.out helloword7_fork_test$

2.5 execvp()函数

  execvp()函数对新程序不需要指定路径,它会从$PATH指定的路径查找,形参是以argv[]的形式。不赘述。

3. fcntl()函数设置FD_CLOEXEC标志位的作用

  前面了解到execX函数簇启动新程序后,新程序的进程空间会覆盖主调进程的进程空间。那么若在主调进程中打开一个文件且将文件描述符传给新程序,会怎样?

// 主调进程int main(void){    int fd;    char buf[10] = {};    fd = open("./test.file", O_RDWR | O_CREAT, 0666);    if (fd < 0)    {        perror("open");        return -1;    }    //fcntl(fd, F_SETFD, FD_CLOEXEC);    sprintf(buf, "%d", fd);     //将文件描述符传给新进程    execl("./new_pro", "./new_pro", buf, NULL);    return 0;}
// 新程序int main(int argc, char* argv[]){    int ret;    char buf[1024] = {};    int fd = atoi(argv[1]);    printf("fd = %d\n", fd);    ret = read(fd, buf, sizeof(buf));    if (ret < 0)    {        perror("read");        return -1;    }    printf("ret = %d\n", ret);    return 0;}

  测试文件test.file的内容为

hello world

  运行结果:

7_fork_test$ ./a.out fd = 3ret = 127_fork_test$

  显然,新程序能使用主调函数传来的文件描述符。但是在主调函数中去除”fcntl(fd, F_SETFD, FD_CLOEXEC);”的注释,那么运行结果:

7_fork_test$ ./a.out fd = 3read: Bad file descriptor7_fork_test$

  可见,新程序已经不能使用主调函数传来的文件描述符了。结论是:
  当文件描述符设置了FD_CLOEXEC,使用execX函数簇时,此描述符会被关闭,导致被该函数簇的调用的新程序不可使用该文件描述符。但是若用fork()创建的子进程,此文件描述符不关闭,仍可以使用,这个可以自行测试。

0 0
原创粉丝点击