深入理解进程控制相关的函数

进程标识

每个进程都有一个非负整型表示的唯一进程 ID。

  • ID 为 0 的进程通常是调度进程,也被称为系统进程。
  • ID 为 1 的进程通常是 init 进程,此进程负责在自举内核后启动一个 UNIX 系统。init 进程决不会终止,它虽然不是内核中的系统进程,但是它以超级用户特权运行。
  • ID 为 2 时页守护进程,此进程负责支持虚拟存储器系统。

有关进程 ID 的一些获取函数

#include 

pid_t getpid(void);     //返回值:调用进程的进程ID

pid_t getppid(void);     //返回值:调用进程的父进程ID

uid_t getuid(void);     //返回值:调用进程的实际用户ID

uid_t geteuid(void);     //返回值:调用进程的有效用户ID

gid_t getgid(void);      //返回值:调用进程的实际组ID

gid_t getegid(void);     //返回值:调用进程的有效组ID

函数 fork

#include 

pid_t fork(void);
//返回值: 子进程返回 0,父进程返回子进程ID;若出错,返回-1

由 fork 创建的新进程被称为子进程。fork 函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是 0,而父进程的返回值则是新建子进程的进程 ID。

子进程和父进程继续执行 fork 调用之后的指令。子进程是父进程的一个副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意:这是子进程拥有的副本。父进程和子进程并不共享这些存储空间。父进程和子进程共享正文段。

由于在 fork 之后经常跟着 exec,所以很多实现并不执行一个父进程数据段、栈和堆的完全副本。而是采用写时复制技术(Copy On Write,COW)。这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读。如果父进程和子进程中的任意一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一页。

fork 失败的两个主要原因:

  • 系统中已经有了太多的进程
  • 该实际用户 ID 的进程总数超过了系统限制

fork 有以下两种用法:

  • 一个父进程希望复制自己,是父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用 fork,使子进程处理此请求。父进程则继续等待下一个服务请求。
  • 一个进程要执行一个不同的程序。在这种情况下,子进程从 fork 返回后立即调用 exec。

函数 wait 和 waitpid

当一个进程正常或异常终止时,内核就向其父进程发送 SIGCHLD 信号。因为子进程终止是个异步事件(可能在父进程运行的任何时候发生),所以这种信号也是内核想父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数。对于这种信号的系统默认动作是忽略它。

当调用 wait 和 waitpid 的进程可能发生:

  • 如果其所有子进程都还在运行,则阻塞
  • 如果一个子进程已终止,正在等待父进程获取其终止状态,则取得该子进程的终止状态立即返回
  • 如果它没有任何子进程,则立即出错返回
#include 

pid_t wait(int *statloc);

pid_t waitpid(pid_t pid, int *statloc,int options);

//返回值:若成功,返回进程ID,若出错,返回0或-1

这两个函数的区别如下:

  • 在一个子进程终止前,wait 使其调用者阻塞,而 waitpid 有一个选项,可使调用者不阻塞。
  • waitpid 并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制他所等待的进程。

这两个函数的参数 statloc 是一个整型指针。如果 statloc 不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指针。

对于 waitpid 函数中 pid 参数的作用解释如下:

  • pid == -1          等待任一子进程。此种情况下,waitpid 与 wait 等效
  • pid > 0             等待进程 ID 与 pid 相等的子进程
  • pid == 0           等待组 ID等于调用进程组 ID 的任一子进程
  • pid < -1等待组 ID 等于 pid 绝对值的任一子进程

waitpid 函数返回终止子进程的进程 ID,并将该子进程的终止状态存放在由 statloc 指向的存储单元中。

options 参数使我们能进一步控制 waitpid 的操作。此参数或者是 0,或者是下列常量按位或运算的结果:

  • WCONTINUED        若实现支持作业控制,那么由 pid 指定的任一子进程在停止后已经继续,但其状态尚未报告,则返回其状态
  • WNOHANG              若由 pid 指定的子进程并不是立即可用的,则 waitpid 不阻塞,此时其返回值为 0
  • WUNTRACEO          若某实现支持作业控制,而由 pid 指定的任一子进程已处于停止状态,并且其状态自停止以来还未报告过,则返回其状态。WIFSTOPPED 宏确定返回值是否对应于一个停止的子进程。

waitpid 函数提供了 wait 函数没有提供的 3 个功能:

  • waitpid 可等待一个特定的进程,而 wait 则返回任一终止子进程的状态。
  • waitpid 提供了一个 wait 的非阻塞版本。
  • waitpid 通过 WUNTRACED 和 WCONTINUED 选项支持作业控制。

内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用 wait 或 waitpid 时,可以得到这些信息。这些信息至少包括进程 ID、该进程的终止状态以及该进程使用的 CPU 时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。

一个以及终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程别称为僵死进程。

函数 exec

 当进程调用一种 exec 函数时,该进程执行的程序完全替换为新程序,而新程序则从其 main 函数开始执行。因为调用 exec 并不创建新进程,所有前后的进程 ID 并未改变。exec 只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。

有七种不同的 exec 函数可供使用,它们通常被称为 exec 函数,我们可以使用这 7 个函数中的任一个。

#include 

int execl(const char *pathname, const char *agr0, ... /* (char *)0 */);

int execv(const char *pathname, char *const argv[]);

int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */);

int execce(const char *pathname, char *const argv[], char *const envp[]);

int execlp(const char *filename, const char *arg0, ... /* (char *)0 */);

int execvp(const char *filename, char *const argc[]);

int fexecve(int fd, char *const argv[], char *const encp[]);

//返回值:若出错,返回 -1;若成功,不返回

这些函数之间的第一个区别时前 4 个函数取路径名作为参数,后两个函数则取文件名作为参数,最后一个函数取文件描述符作为参数。

当指定 filename 作为参数时:

  • 如果 filename 中包含 /,则就将其视为路径名
  • 否则就按 PATH 环境变量,在它所指定的各目录中搜寻可执行文件,PATH 变量包含了一张目录表(称为路径前缀),目录之间用冒号 : 分割。

第二个区别与参数表的传递有关(l 表示列表 list, v 表示适量 vector)。函数 execl、execlp 和 execle 要求将新程序的每个命令行参数都说明为一个单独的参数。这种参数表以空指针结尾。对于另外 4 个函数(execv、execvp、execve 和 fexecve),则应先构造一个指向各参数的指针数组,然后将该数组地址作为这 4 个函数的参数。

最后一个区别与想向程序传递环境表相关。以 e 结尾的 3 个函数(execle、execve 和 fexecve)可以传递一个指向环境字符串指针数组的指针。其他 4 个函数则使用调用进程中的 environ 变量为新程序复制现有的环境。

更改用户 ID 和更改组 ID 函数

#inlcude 

int setuid(uid_t uid);

int setgid(gid_t fid);

//返回值:若成功,返回0;若出错,返回-1

更改用户 ID 的规则(关于用户 ID 我们所说明的一切都适用于组 ID)

  • 若进程具有超级用户特权,则 setuid 函数将实际用户 ID、有效用户 ID 以及保存的设置用户 ID 设置为 uid
  • 若进程没有超级用户特权,但是 uid 等于实际用户 ID 或保存的设置用户 ID,则 setuid 只将有效用户 ID 设置为 uid。不更改实际用户 ID 和保存的设置用户 ID。
  • 如果上面两个条件都不满足,则 errno 设置为 EPERM,并返回-1。

关于内核所维护的 3 个用户 ID,还要注意以下几点:

  •  只有超级用户进程可以更改实际用户 ID。通常,实际用户 ID 是在用户登录时,由 login 程序设置的,而且绝不会改变它。
  • 仅当对程序文件设置了设置用户 ID 位时,exec 函数才设置有效用户 ID。如果设置用户 ID 位没有设置,exec 函数不会改变有效用户 ID,而将维持其现有值。
  • 保存的设置用户 ID 是由 exec 复制有效用户 ID 而得到的。如果设置了文件的设置用户 ID 位,则在 exec 根据文件的用户 ID 设置了进程的有效用户 ID 以后,这个副本就被保存起来了。

函数 system

#include 

int system(const char *cmdstring);

如果 cmdstring 是一个空指针,则仅当命名处理程序可用时,system 返回非 0 值,这一特征可用确定在一个给定的操作系统上是否支持 system 函数。

因为 system 在其实现中调用了 fork、exec 和 waitpid,因此有 3 种返回值:

  1. fork 失败或者 waitpid 返回处理 EINTR 之外的出错,则 system 返回 -1,并且设置 errno 以指示错误类型。
  2. 如果 exec 失败(表示不能执行 shell),则其返回值如同 shell 执行了 exit(127) 一样。
  3. 否则所有 3 个函数(fork、exec 和 waitpid)都成功,那么 system 的返回值时 shell 的终止状态。

你可能感兴趣的:(UNIX笔记专区,数据库,linux,运维)