进程控制相关 API-创建进程、进程分离、进程退出、进程阻塞

目录

进程控制相关 API

父进程创建子进程 fork()

进程分离 exec 族函数

进程的退出 return/exit()

进程的阻塞 wait()

其它 API


进程控制相关 API

p.s 进程控制中的状态转换 相关 API,用户很少用到,在此不提。

一般来说,这些内核标准 API,在执行出错(可能是资源不够、权限不够等等)会返回负值(比如 -1),并设置 errno 值。

父进程创建子进程 fork()

在 Linux 中,为了创建一个子进程,父进程用系统调用 fork() 来创建子进程。fork() 其实就是把父进程复制了一份(子进程有自己的特性,比如标识、状态、数据空间(堆栈区和数据区)等(这些是子进程独有的);子进程和父进程共同使用程序代码、共用时间片(这些是共有的)等)。

通常在调用fork函数之后,程序会设计一个if选择结构。当PID等于0时,说明该进程为子进程,那么让它执行某些指令,比如说使用exec库函数(library function)读取另一个程序文件,并在当前的进程空间执行 (这实际上是我们使用fork的一大目的: 为某一程序创建进程);而当PID为一个正整数时,说明为父进程,则执行另外一些指令。由此,就可以在子进程建立之后,让它执行与父进程不同的功能。

pid_t fork();fork() 对子进程 返回 0,对父进程 返回 子进程的 ID,返回 小于 0 值为出错。

#include<stdio.h>
#include<unistd.h>
​
int main()
{
  int p_num = 0;
  int c_num = 0;
  int pid = fork();
  if(pid == 0) // 返回的pid为0为子进程
  {
        c_num++;
  }
  else
  {
        p_num++; // 返回的pid大于0为父进程
  }
  printf("p_num=%d, c_num=%d\n",p_num,c_num);
  printf("pid=%d\n",pid);
  return 0;
}
​
// 运行结果如下所示
p_num=1, c_num=0
pid=36101
p_num=0, c_num=1
pid=0

子进程总可以查询自己的 PPID 来知道自己的父进程是谁,这样,一对父进程和子进程就可以随时查询对方。

其它:

  • fork() 的 写时复制 概念,可网搜了解。即 用到某个资源时候才会复制,不需要修改的资源不会复制,尽量推迟高系统消耗的操作直到必要时才会执行。

  • vfork() 不常用,实现可能不会完全没问题,概念可网搜来了解。

进程分离 exec 族函数

通过 fork 后,子进程并没有和父进程独立开,用的是相同的代码。另外还有一个问题时,这个时候子进程的时间片是和父进程一分为二来共享的。为了彻底将父进程和子进程分离开来,就要用到一个系统调用 exec 族函数,这是读取另一个程序文件,并在当前的进程空间执行。当我们创建了一个进程之后,通常将子进程替换成新的进程映象,这可以用 exec 系列的函数来进行,且新进程与原进程有相同的 PID。

参考 Linux下exec函数族(execl,execv,execle,execve,execlp,execvp,fexecve)的使用和对比leumber的博客-CSDN博客exec和execvexec系列函数(execl,execlp,execle,execv,execvp)使用_gauss的博客-CSDN博客 这个里面有每个 API 的使用例子,exec和execv区别 - CSDN

  • 各个 API 原型:#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[]);

    传入参数:path 参数表示你要启动程序的名称包括路径名;file 参数表示 要启动的 程序 / 文件 的文件名(系统从环境变量 PATH 里面寻找该程序,因此不用带路径全名);arg 参数表示启动程序所带的参数,一般第一个参数为要执行命令名,不是带路径且arg必须以NULL结束;

    返回值:成功返回0,失败返回-1。

  • exec 族函数名中 l 表示列表 list,v 表示数组。

    • execl、execlp、execle 将新程序的每个命令行参数都以一个单独的参数,这种参数列表以NULL结尾。

    • execv、execvp、execve 和 fexecve 则应先构造一个指向各参数的指针数组,然后将该数组地址作为参数传入。

  • exec 族函数名中 p 结尾表示函数第一个参数取 filename。

    • execlp、execvp与其他函数不同就是第一个参数取filename,用 PATH 环境变量寻找可执行文件,filename 既可以是文件路径加程序名,也可以是PATH环境变量下的 /sbin: /bin: /usr/bin: 即shell命令。

  • exec 族函数名中 e 结尾表示可以传递环境表信息 environ。

    • execle、execve、fexecve 可以传递一个指向环境字符串指针数组的指针。

// process.c
#include<stdio.h>
#include<unistd.h>
​
int main()
{
    int pid = fork();
    if(pid == 0)
    {
        execv("./test.o",NULL);  // test.o是一个经过编译的c语言文件,这里记得要放test.o的绝对路径
    }
    printf("This is parent process\n");
    return 0;
}
​
// test.c
#include<stdio.h>
int main()
{
    printf("This is child process");
    return 0;
}
​
// 运行结果如下所示
This is parent process
This is child process

exec 族函数的使用例子

/* exec.c */
#include <unistd.h>
main()
{
    char *envp[]={"PATH=/tmp","USER=lei","STATUS=testing",NULL}; /* 数组 必须以 NULL 做结尾 */
    char *argv_execv[]={"echo", "excuted by execv", NULL};
    char *argv_execvp[]={"echo", "executed by execvp", NULL};
    char *argv_execve[]={"env", NULL};
    if(fork()==0)
        if(execl("/bin/echo", "echo", "executed by execl", NULL)<0) /* 路径全名,传入参数写全,以NULL结尾 */
            perror("Err on execl");
    if(fork()==0)
        if(execlp("echo", "echo", "executed by execlp", NULL)<0) /* 只写执行程序的文件名,系统会去 PATH 环境变量寻找 */
            perror("Err on execlp");
    if(fork()==0)
        if(execle("/usr/bin/env", "env", NULL, envp)<0) /* 可传入环境变量 */
            perror("Err on execle");
    if(fork()==0)
        if(execv("/bin/echo", argv_execv)<0) /* 带 v 的就是 传入参数 以 指针数据(字符串数据)传入,其它与上面的 API 一样 */
            perror("Err on execv");
    if(fork()==0)
        if(execvp("echo", argv_execvp)<0)
            perror("Err on execvp");
    if(fork()==0)
        if(execve("/usr/bin/env", argv_execve, envp)<0)
            perror("Err on execve");
}
​
/* 执行 ./exec 后返回:
executed by execl
PATH=/tmp
USER=lei
STATUS=testing
executed by execlp
excuted by execv
executed by execvp
PATH=/tmp
USER=lei
STATUS=testing
*/

exec 族函数 的常见的错误返回(exec 返回 -1,并设置 errno 为以下的值):

  • 找不到文件或路径,此时errno被设置为ENOENT;

  • 数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT;

  • 没有对要执行文件的运行权限,此时errno被设置为EACCES。

  • 等等,有很多种类型的错误返回。

更多要注意的地方:

  • 实际操作时, 一般在 调用 exec 函数之前 关闭所有已经打开的文件。也可以通过 fcntl() 让内核去完成。

进程的退出 return/exit()

参考 进程的几种退出机制_Leon_George的博客-CSDN博客操作系统 — 进程的退出(exit)Dawn_sf的博客-CSDN博客exit 进程退出 exit函数及与return的区别_panda19881的博客-CSDN博客

几种退出方式

  • 正常退出

    1. 在main()函数中执行return(renturn执行完后把控制权交给调用函数)。

    2. 调用exit()函数(exit执行完后把控制权交给系统)。

    3. 调用_exit()函数(同上)。

  • 异常退出

    1. 调用abort函数(exit是正常终止进程,abort是异常终止,突出在异常)。

    2. 进程收到某个信号,而该信号使程序终止。

exit()_exit()区别exit() 函数是在_exit()函数之上的一个封装,其会调用_exit(),并在调用之前先刷新流(stdin, stdout, stderr ...),即 把文件缓冲区的内容写回文件。exit在头文件stdlib.h中声明,而_exit()声明在头文件unistd.h中声明。使用 exit() 更安全。exit中的参数exit_code为0代表进程正常终止(即 exit(0);),若为其他值表示程序执行过程中有错误发生。

exitreturn 区别:如果return 或者exit出现在main函数中,两者的作用是一样(即 return 0;exit(0); 一样)。如果return出现在子程序中表示返回(仅意味着 退出/结束 其所在的函数或子进程),而exit出现在子进程中表示终止子进程。

但不管是哪种退出方式,系统最终都会执行内核中的某一代码。这段代码用来关闭进程所用已打开的文件描述符,释放它所占用的内存和其他资源。

父子进程终止的先后顺序不同会产生不同的结果

  • 子进程先于父进程终止,而父进程调用了wait函数:子进程退出并被父进程回收(好)

此时父进程会等待子进程结束。

  • 父进程先于子进程终止:子进程变孤儿进程(中)

此种情况就是我们前面所用的孤儿进程。当父进程先退出时,系统会让init进程接管子进程 。孤儿进程会被过继给init进程,init进程也就成了该进程的父进程。init进程负责该子进程终结时调用wait函数。init进程会在有子进程退出时调用wait函数。

  • 子进程先于父进程终止,而父进程又没有调用wait函数:子进程变僵死进程(坏)

此种情况子进程进入僵死状态,并且会一直保持下去直到系统重启。子进程处于僵死状态时,内核只保存进程的一些必要信息以备父进程所需。此时子进程始终占有着资源,同时也减少了系统可以创建的最大进程数。

因此要尽量避免这种情况发生,否则 僵死进程 不但占用着资源,而且可能越积累越多。一个糟糕的程序也完全可能造成子进程的退出信息滞留在内核中的状况(父进程不对子进程调用wait函数),这样的情况下,子进程成为僵尸(zombie)进程。当大量僵尸进程积累时,内存空间会被挤占。

僵死状态:一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息,释放它仍占有的资源)的进程被称为僵死进程(zombie)。

僵死进程 和 孤儿进程 的释义:

总结:这三种进程退出,最好情况是 父进程调用 wait 正常回收 终止的子进程,其次是 父进程提前终止 / 子进程称为孤儿进程 / 子进程被过继给 init 进程 / init进程会在有子进程退出时调用wait函数,最坏是 父进程没有调用 wait 而子进程退出 / 子进程成为 僵死进程。

注册 进程退出时 调用的函数:#include <stdlib.h> int atexit (void (*function)(void)); ,注册的函数会在 调用 exit(); 或 从 main 退出 或 收到终止进程的信号(SIGTERM 或 SIGKILL)时候被调用一次。注册的函数内不能再调用 exit(),否则会引起无限递归。

进程的阻塞 wait()

处于运行状态的进程,在其运行过程中期待某一事件的发生,如等待键盘的输入、等待磁盘数据传输完成、等待其他进程发送信息,当被等待的时间未发生时,由进程自己执行阻塞原语,使自己的运行状态变为阻塞态。

即 休眠/阻塞 来 等待 信号(signal)父进程等待子进程退出(再回收其资源)

p.s 对于 等待 信号(signal)在 下面 进程间通讯(IPC) 里的 信号(Signal) 一节介绍。这里只讨论 父进程等待子进程退出(再回收其资源)。

引用 exec和execv区别 - CSDN(回过头意识到,我就相当于把格式整理地更漂亮了一遍。。)。

父进程 wait 阻塞来等待子进程退出(再回收其资源)

子进程终止时,内核会向其父进程 发送 SIGCHILD 信号。

当子进程终结时,它会通知父进程,并清空自己所占据的内存,并在内核里留下自己的退出信息(exit code,如果顺利运行,为0;如果有错误或异常状况,为>0的整数)。父进程在得知子进程终结时,有责任对该子进程使用wait系统调用(否则子进程就成了僵死进程,应当尽量避免)。这个wait函数能从内核中取出子进程的退出信息,并清空该信息在内核中所占据的空间。

#include <sys/types.h> /* 提供类型pid_t的定义 */ 
#include <sys/wait.h> 
pid_t wait(int *status);

进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何dump掉的毫不在意,只想把这个僵尸进程消灭掉,我们就可以设定这个参数为NULL。

如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

例子:

/* wait1.c */
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
main()
{
    pid_t pc, pr;
    pc = fork();
    if(pc < 0)            /* 如果出错 */
        printf("error ocurred!\n");
    else if(pc == 0){    /* 如果是子进程 */ 
        printf("This is child process with pid of %d\n",getpid());
        sleep(10);       /* 睡眠10秒钟 */
    }
    else{                 /* 如果是父进程 */
        pr = wait(NULL); /* 在这里等待子进程的退出,并不在意其 exit 退出返回值 */
        printf("I catched a child process with pid of %d\n"),pr);
    }
    exit(0);
}

对 退出返回值做进一步分析的宏:

  • WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。(请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数--指向整数的指针status,而是那个指针所指向的整数)

  • WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出WEXITSTATUS(status)就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说,WIFEXITED返回0,这个值就毫无意义。

另一篇文章再讲一遍:

pid_t wait(int *status);,wait 系统调用会使父进程阻塞直到一个子进程结束或者是父进程接受到了一个信号。如果没有父进程没有子进程或者他的子进程已经结束了 wait 回立即返回。成功时(因一个子进 程结束)wait 将返回子进程的 ID,否则返回-1,并设置全局变量 errno。status 是子进程的 退出状态。子进程调用 exit / _exit 或者是 return 来设置这个值。为了得到这个值 Linux 定 义了几个宏来测试这个返回值。

另一篇文章具体讲几个宏的区别:

#include <sys/wait.h>
int WIFEXITED (status);
int WIFSIGNALED (status);
int WIFSTOPPED (status);
int WIFCONTINUED (status);
int WEXITSTATUS (status);
int WTERMSIG (status);
int WSTOPSIG (status);
int WCOREDUMP (status);

waitpid 阻塞等待特定 pid 的子进程退出

#include <sys/types.h> /* 提供类型pid_t的定义 */
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);

waitpid() 参数说明:

  • pid:

    pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。

    pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。

    pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。

    pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

  • status:承接子进程的 exit 退出返回值,不用时填 NULL。

  • options:options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用 "|" 运算符把它们连接起来使用,如果我们不想使用它们,也可以把options设为0(不用时设为 0)。

    WNOHANG:即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。

    WUNTRACED:涉及到一些跟踪调试方面的知识,加之极少用到,有兴趣的读者可以自行查阅相关材料。

  • 返回值:

    waitpid的返回值比wait稍微复杂一些,一共有3种情况:

    1. 当正常返回的时候,waitpid返回收集到的子进程的进程ID;

    2. 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;

    3. 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

    当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD。

    子进程的结束状态返回后存于status,底下有几个宏可判别结束情况:

    • WIFEXITED(status)如果子进程正常结束则为非0值。

    • WEXITSTATUS(status)取得子进程exit()返回的结束代码,一般会先用WIFEXITED 来判断是否正常结束才能使用此宏。

    • WIFSIGNALED(status)如果子进程是因为信号而结束则此宏值为真。

    • WTERMSIG(status)取得子进程因信号而中止的信号代码,一般会先用WIFSIGNALED 来判断后才使用此宏。

    • WIFSTOPPED(status)如果子进程处于暂停执行情况则此宏值为真。一般只有使用WUNTRACED 时才会有此情况。

    • WSTOPSIG(status)取得引发子进程暂停的信号代码。

其它 API

头文件:

#include <unistd.h>;
#include <pwd.h>;
#include <sys/types.h>;
  • pid_t getpid(void);,得到当前进程 pid。

    pid_t getppid(void);,得到当前进程的父进程的 pid。

  • 进程的实际用户、有效用户相关概念:

  • 改变实际用户 API:

  • 改变有效用户 API:

  • 得到用户 ID:

    uid_t getuid(void);uid_t geteuid(void);,分别得到进程的所有者用户的 ID 和 有效用户 ID。 gid_t getgid(void);git_t getegid(void);,分别得到组 ID 和有效组 ID。

  • 得到用户的更多信息:

    struct passwd { /* 这个结构体在 types.h 里面定义 */
        char *pw_name; /* 登录名称 */ 
        char *pw_passwd; /* 登录口令 */ 
        uid_t pw_uid; /* 用户 ID */ 
        gid_t pw_gid; /* 用户组 ID */ 
        char *pw_gecos; /* 用户的真名 */ 
        char *pw_dir; /* 用户的目录 */ 
        char *pw_shell; /* 用户的 SHELL */ 
    }; 
    struct passwd *getpwuid(uid_t uid); /* 返回 用户 ID 为 uid 的 用户信息的 struct passwd 结构体指针*/
  • sleep(x);,阻塞/延时 x 秒时间。

  • strerror(errno),返回一个指定的错误号的错误信息的字符串。

  • etc.

猜你喜欢

转载自blog.csdn.net/Staokgo/article/details/132630662