《Linux C编程实战》笔记:进程操作之退出,执行,等待

进程退出

进程退出表示进程即将运行结束。在Linux中退出分为正常退出和异常退出。

正常退出:

  1. 在main函数中执行return
  2. 调用exit函数
  3. 调用_exit函数

异常退出:

  1. 调用abort函数
  2. 收到某个信号,这个信号是程序终止

退出方式比较

  1. exit和return的区别:exit是一个函数,有参数;而return是函数执行完后的返回。exit把控制权交给系统,而return将控制权交给调用函数。
  2. exit和 abort的区别:exit是正常终止进程,而about是异常终止。
  3. exit(int exit_code): exit中的参数exit_code为0代表进程正常终止,若为其他值表示程序执行过程中有错误发生,比如溢出、除数为0。
  4. exit()和_exit()的区别:exit在头文件stdlib.h中声明,而_exit()声明在头文件unistd.h中。两个函数均能正常终止进程,但是_exit()会执行后立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。

父子进程终止的先后顺序不同会产生不同的结果。在子进程退出前父进程先退出,则系统会让 init进程接管子进程(前面有代码演示过)。当子进程先于父进程终止,而父进程又没有调用wait 函数等待子进程结束,子进程进入僵死状态,并且会一直保持下去除非系统重启。子进程处于僵死状态时,内核只保存该进程的一些必要信息以备父进程所需。此时子进程始终占用着资源,同时也减少了系统可以创建的最大进程数。如果子进程先于父进程终止,且父进程调用了wait或waitpid函数,则父进程会等待子进程结束。

执行新程序

使用fork或vfork创建子进程后,子进程通常会调用exec函数来执行另外一个程序。系统调用exec用于执行一个可执行程序以代替当前进程的执行映像。

注意:exec调用并没有生成新进程。一个进程一旦调用exec函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,惟一保留的就是进程ID。也就是说,对系统而言,还是同一个进程,不过执行的已经是另外一个程序了。

Linux下,exec函数族又6种不同的调用形式,如下:

  1. execl

    • int execl(const char *path, const char *arg, ...);
    • 这个函数接受一个以空格分隔的参数列表,最后一个参数必须为 NULL

    execl("/bin/ls", "ls", "-l", (char *)NULL);

  2. execv

    • int execv(const char *path, char *const argv[]);
    • 这个函数接受一个参数数组,最后一个元素必须为 NULL

    char *args[] = {"ls", "-l", NULL}; execv("/bin/ls", args);

  3. execle

    • int execle(const char *path, const char *arg, ..., char *const envp[]);
    • execl 相似,但可以指定新程序的环境变量。

    execle("/bin/ls", "ls", "-l", (char *)NULL, envp);

  4. execve

    • int execve(const char *path, char *const argv[], char *const envp[]);
    • execv 相似,但可以指定新程序的环境变量。

    char *args[] = {"ls", "-l", NULL}; execve("/bin/ls", args, envp);

  5. execlp

    • int execlp(const char *file, const char *arg, ...);
    • execl 类似,但会在 PATH 环境变量指定的目录中查找可执行文件。

    execlp("ls", "ls", "-l", (char *)NULL);

  6. execvp

    • int execvp(const char *file, char *const argv[]);
    • execv 类似,但会在 PATH 环境变量指定的目录中查找可执行文件。

    char *args[] = {"ls", "-l", NULL}; execvp("ls", args);

这些函数执行成功时不会返回,而是将当前进程的映像替换为新的程序。如果函数调用失败,它们会返回 -1,并设置 errno 变量以指示错误的原因。

为了更好理解exec,首先要理解环境变量这个概念。Linux引入了环境变量的概念,包括用户的主目录,终端类型、当前目录等,它们定义了用户的工作环境,所以称为环境变量。可以使用env命令查看环境变量值,用户也可以修改这些变量值以定制自己的工作环境

示例程序1

演示环境变量的应用

#include<cstdlib>
#include<malloc.h>
#include<cstring>
#include <cstdio>
#include<ctime>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<cerrno>
using namespace std;
extern char **environ;
int main(int argc,char *argv[]){
    int i;
    printf("Argument:\n");
    for(int i=0;i<argc;i++)
        printf("argv[%d] is %s\n",i,argv[i]);
    printf("Environment:\n");
    for(int i=0;environ[i]!=nullptr;i++)
        printf("%s\n",environ[i]);
    return 0;
}

还有很多环境变量,图片放不下了

命令行里输入命令env,环境变量和程序里应该是一样的

在C语言中,extern char **environ 是一个用于访问进程环境变量的外部变量声明。这个变量通常由操作系统或C库提供,用于存储当前进程的环境变量。

每个C程序在运行时都有一个环境变量表,其中包含了键值对形式的环境变量。environ 是一个指向字符串指针数组的指针,每个字符串指针指向一个环境变量的字符串。

通过访问 environ 变量,你可以迭代这个字符串指针数组,直到遇到一个空指针,表示环境变量列表的结束。每个字符串指针指向一个形如 "key=value" 的字符串,表示一个环境变量。

有一些系统和编译器提供了一个额外的参数,通常称为 envp,它是一个指向环境变量的字符串数组的指针。

int main(int argc, char *argv[],char **envp);

打印envp,也可以得到环境变量

int main(int argc,char *argv[],char **envp){
    int i;
    printf("Argument:\n");
    for(int i=0;i<argc;i++)
        printf("argv[%d] is %s\n",i,argv[i]);
    printf("Environment:\n");
    for(int i=0;envp[i]!=nullptr;i++)
        printf("%s\n",envp[i]);
    return 0;
}

这是main函数为上面形式的运行结果,确实可以打印环境变量

事实上无论是哪个exec函数,都是将可执行程序的路径、命令行参数和环境变量3个参数传递给可执行程序的main函数。

上面说到如果失败,即遇到错误的事件,exec函数会返回-1,以下是一些错误

 在 Linux 操作系统下,exec函数族可以执行二进制的可执行文件,也可以执行Shell脚本程序,但Shell脚本必须以下面所示的格式开头:第一行必须为:#! interpretername [arg]。其中 interpretername可以是Shell或其他解释器,例如,/bin/sh 或usr/bin/perl,arg是传递给解释器的参数。

示例程序2

演示exec函数的用法

processimage.cpp

这里写的是子进程到时候执行exec后执行的代码

#include<cstdio>
#include<sys/types.h>
#include<unistd.h>
int main(int argc,char *argv[],char **environ){
    int i;
    printf("I am a process image!\n");
    printf("My pid= %d,parent=%d\n",getpid(),getppid());
    printf("uid = %d,gid = %d\n",getuid(),getgid());
    for(i=0;i<argc;i++)
        printf("argv[%d]:%s\n",i,argv[i]);
}

getuidgetpid 是两个与进程和用户身份相关的系统调用函数。

  1. getuid 函数:

    • uid_t getuid(void);
    • 用于获取当前进程的用户实际用户标识(User ID)。返回值是用户的实际用户ID。
  2. getpid 函数:

    • pid_t getpid(void);
    • 用于获取当前进程的进程ID。返回值是当前进程的进程ID。

execve.cpp

#include<cstdio>
#include<sys/types.h>
#include <sys/wait.h>
#include<unistd.h>
#include<cstdlib>
int main(int argc,char *argv[],char **environ){
    pid_t pid;
    int stat_val;
    printf("Exec example\n");
    pid=fork();
    switch (pid)
    {
    case -1:
        perror("Process Creation failed\n");
        exit(1);
    case 0:
        printf("Child process is running\n");
        printf("My pid =%d,parentpid= %d\n",getpid(),getppid());
        printf("uid=%d,gid=%d\n",getuid(),getgid());
        execve("processimage",argv,environ);
        printf("process never go to here!\n");//是不会允许到这一句的
    default:
        printf("Parent process is running\n");
        break;
    }
    wait(&stat_val);
    exit(0);
}

注意顺序,先编译第一个程序,而且可执行文件名称要对应execve的参数

可以看到新程序进程的pid,ppid,uid和gid都保持了原来子进程的。调用execve之后,原有的子进程的映像被替代,所以那句打印永远不会执行

wait 函数的原型如下:

#include <sys/types.h>

#include <sys/wait.h>

pid_t wait(int *status);

  • pid_t 是进程ID的数据类型,通常是整数。
  • int *status 是一个指向整数的指针,用于存储子进程的终止状态。

wait 函数用于等待任意一个子进程的退出,并获取子进程的退出状态。其返回值是已经终止的子进程的进程ID。如果调用失败,返回值为 -1。

status 参数用于存储有关子进程终止状态的信息。如果不关心子进程的退出状态,可以将 status 设置为 NULL如果 status 不是 NULL,则可以使用一些宏来检查 status 中的信息,例如:

  • WIFEXITED(status):如果子进程正常终止(不是信号终止),返回非零。
  • WEXITSTATUS(status):获取子进程的退出状态,只有在 WIFEXITED(status) 为真时才有效。

执行新程序后的进程除了保持了原来的进程ID、父进程ID、实际用户ID和实际组ID之外,进程还保持了许多原有特征,主要有。

  1. 当前工作目录。
  2. 根目录。
  3. 创建文件时使用的屏蔽字。
  4. 进程信号屏蔽字。
  5. 未决警告。
  6. 和进程相关的使用处理器的时间。
  7. 控制终端。
  8. 文件锁。

等待进程结束

之前提到的僵死状态,如果父进程调用了wait或waitpid,就不会使子进程编程僵尸进程

#include<sys/types.h>
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc,int options);

wait函数使父进程暂停执行,直到它的一个子进程结束为止。该函数的返回值是终止运行的子进程的PID。参数 statloc所指向的变量存放子进程的退出码(注意statloc本身不等于退出码),即从子进程的main 函数返回的值或子进程中exit函数的参数。如果statloc不是一个空指针,状态信息将被写入它指向的变量。

头文件sys/wait.h中定义了解读进程退出状态的宏。

注:status就是通过statloc返回来的值

  1. WIFEXITED(status)

    • 如果子进程正常终止(不是由于信号),则返回非零值。
  2. WEXITSTATUS(status)

    • 如果 WIFEXITED(status) 为真,该宏返回子进程的退出状态。这是子进程传递给 exit_exit 函数的值。
  3. WIFSIGNALED(status)

    • 如果子进程是因为信号而终止,而不是正常退出,则返回非零值。
  4. WTERMSIG(status)

    • 如果 WIFSIGNALED(status) 为真,该宏返回导致子进程终止的信号编号。
  5. WIFSTOPPED(status)

    • 如果子进程当前已经停止,则返回非零值。这通常是由于接收到一个信号,要求进程停止执行。
  6. WSTOPSIG(status)

    • 如果 WIFSTOPPED(status) 为真,该宏返回导致子进程停止的信号编号。
  7. WIFCONTINUED(status)

    • 如果子进程由于接收到 SIGCONT 信号而继续运行,则返回非零值。

waitpid也用来等待子进程的结束,但它用于等待某个特定进程结束。参数pid指明要等待的子进程的PID。 参数 statloc的含义与wait函数中的statloc相同。options参数允许用户改变waitpid的行为,若将该参数赋值为WNOHANG,则使父进程不被挂起而立即返回并执行其后的代码。

下面是 waitpid 函数中 pid 参数不同取值的意义:

  1. pid > 0:

    • 表示等待具有进程ID为 pid 的子进程。
    • 如果该子进程还没有退出,则父进程会阻塞等待,直到子进程退出为止。
    • 如果子进程已经退出,父进程会立即返回。
  2. pid == -1:

    • 表示等待任意子进程,类似于 wait 函数。
    • 父进程会等待第一个终止的子进程,无论其进程ID是多少。
  3. pid == 0:

    • 表示等待与调用 waitpid 的父进程在同一个进程组的任意子进程。
    • 这对于等待同一作业中的任意子进程很有用。
  4. pid < -1:

    • 表示等待进程组ID为 pid 的任意子进程。
    • 这对于等待特定进程组中的任意子进程很有用。

以下是一些常用的 waitpid 选项及其对应的宏:

  1. WNOHANG:

    • 启用非阻塞模式,即如果没有子进程退出,则立即返回,而不会阻塞父进程。
  2. WUNTRACED:

    • 用于获取已经停止但尚未进入终止状态的子进程的状态信息。
  3. WCONTINUED:

    • 用于获取已经继续运行的子进程的状态信息。

这些宏可以与位运算结合使用,以同时指定多个选项。

如果想让父进程周期性地检查某个特定的子进程是否已经退出,可以按如下方式调用waitpid。

waitpid(child_pid, NULL,WNOHANG);

如果子进程尚未退出,它将返回0;如果子进程已经结束,则返回child _pid。调用失败时返回-1。失败的原因包括没有该子进程、参数不合法等。

注意:wait等待第一个终止的子进程,而 waitpid则可以指定等待特定的子进程。waitpid 提供了一个wait的非阻塞版本。有时希望取得一个子进程的状态,但不想使父进程阻塞,waitpid 提供了一个这样的选项: WNOHANG,它可使调用者不阻塞。如果一个没有任何子进程的进程调用wait函数,会立即出错返回。

示例程序3

演示wait的使用和子进程退出码的获得

#include<cstdlib>
#include<malloc.h>
#include<cstring>
#include <cstdio>
#include<ctime>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/wait.h>
#include<fcntl.h>
#include<unistd.h>
#include<cerrno>
using namespace std;
int main(){
    pid_t pid;
    const char *msg;
    int k;
    int exit_code;
    printf("Study how to get exit code\n");
    pid=fork();
    switch (pid)
    {
    case 0:
        msg="Child process is running";
        k=5;
        exit_code=37;//这就是子进程的退出码
        break;
    case -1:
        perror("Process creation failed\n");
        exit(1);
    default:
        exit_code=0;
        break;
    }
    if(pid!=0){//父进程会执行这里
        int stat_val;
        pid_t child_pid;
        child_pid=wait(&stat_val);//父进程暂停等待子进程结束
        printf("Child process has exited,pid=%d\n",child_pid);
        if(WIFEXITED(stat_val))//通过这个判断子进程是否是正常结束的
            printf("Child exited with code %d\n",WEXITSTATUS(stat_val));//获得退出码
        else printf("Child exited abnormally\n");
    }
    else{//子进程会执行这里
        while (k-->0)
        {
            puts(msg);
            sleep(1);
        }
    }
    exit(exit_code);
}

运行结果:

  • 14
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值