APUE 一书的第八章学习笔记。

进程标识

大家都知道使用 PID 来标识的。

系统中的一些特殊进程:

  • PID = 0: 调度进程,也称为交换进程 (Swapper)
  • PID = 1: init 进程,自检结束后由内核调用,读取与系统初始化相关的文件,如 /etc/init.d/*, /etc/rc*/ . init 进程是一个以 root 启动的普通进程,而不是像 Swapper 是一个内核进程。init 是所有孤儿进程的父进程。
  • PID = 2: 页守护进程 (Page Daemon), 为虚拟存储器的分页操作提供支持。

关于进程标识的 API :

  1. #include <unistd.h>
  2. pid_t getpid(void); // Returns: process ID of calling process
  3. pid_t getppid(void); // Returns: parent process ID of calling process
  4. uid_t getuid(void); // Returns: real user ID of calling process
  5. uid_t geteuid(void); // Returns: effective user ID of calling process
  6. gid_t getgid(void); // Returns: real group ID of calling process
  7. gid_t getegid(void); // Returns: effective group ID of calling process

fork

  1. #include <unistd.h>
  2. pid_t fork(void); // Returns: 0 in child, process ID of child in parent, −1 on error

fork 的一些特点:

  • 调用 1 次,返回 2 次;
  • 为什么将子进程的 ID 返回给父进程?一个进程可有多个子进程,但没有函数可以获得所有子进程的 ID 。
  • 为什么 fork 返回给子进程的是 0 ?因为子进程的 PID 不可能为 0 ,它的父进程 PID 可以由 getppid() 获取。

fork 返回后,父子进程都会在 fork 的调用点继续执行。子进程会获得父进程的数据空间、堆和栈的副本,但应当注意的是子进程拥有的是副本,而不是父子进程一同共享这些数据。父子进程共享的只有程序的 text 段。

由于 fork 之后经常会跟着 exec 函数,所以很多时候并不修改父进程的数据段和堆栈。为了针对这一特点进行优化,实现当中采用了写时复制 (Copy On Write), 父子进程共享这些区域,但内核会将它们的权限修改为只读 (Read-Only). 如果父子进程中的一个试图修改这些区域,则内核只会为被修改区域的那块内存拷贝一份副本,通常是虚拟存储器中的“一页”。

一般来说,fork 之后,父子进程是并发执行的,为此还需要实现进程间的同步操作(例如信号)。

fork 一般有 2 种常见用法:

  1. 父进程复制自己,父子进程同时执行不同的代码段。这种情况常见于网络服务进程:父进程等待客户端的请求,当请求到达时,父进程调用 fork ,使子进程处理该请求,而父进程继续等待下一请求。
  2. 一个进程需要执行不同的程序。例如 Shell 程序,子进程从 fork 返回之后调用 exec 系列函数。在某些系统中,会把 fork, exec 封装为一种操作 spawn .

例子

  1. #include "apue.h"
  2. int globalvar = 123;
  3. char buf[] = "a write to stdout\n";
  4. int main()
  5. {
  6. int var = 233;
  7. pid_t pid;
  8. if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1) err_sys("write err\n");
  9. if ((pid = fork()) < 0) err_sys("fork err\n");
  10. else if (pid == 0) var++, globalvar++;
  11. else sleep(2);
  12. printf("pid=%d, globalvar=%d, var=%d\n", pid, globalvar, var);
  13. return 0;
  14. }

输出:

  1. $ ./a.out
  2. a write to stdout
  3. pid=0, globalvar=124, var=234
  4. pid=15438, globalvar=123, var=233

文件共享

fork 之后,子进程会拥有父进程的文件描述符表的副本,如下图所示。

所以:

  • 父进程的重定向dup也会被子进程继承。
  • 父子进程共享某一打开文件的偏移量。如果父子进程同时对该文件进行写操作(但没有任何同步机制),那么就会造成数据的混乱。

vfork

  1. #include <sys/types.h>
  2. #include <unistd.h>
  3. pid_t vfork(void);

vfork 用于创建一个子进程,而该子进程的目的是执行 exec 系列函数。

vfork 并不会把父进程的地址空间完全复制到子进程中,因为考虑到子进程会马上调用 exec (因而不会引用该地址空间的数据),不过在它调用 exec, exit 之前,它一直在父进程的地址空间中运行。但如果子进程后续没有调用 exec 或者 exit,是一种未定义行为。

vforkfork 的另外一个重要区别是:vfork 保证子进程先运行,在它调用 exec, exit 之后父进程才可能被调度运行(如果这 2 个调用依赖于父进程的进一步动作,那么会产生死锁)。

例子

  1. int globvar = 6;
  2. int main()
  3. {
  4. int var = 88;
  5. pid_t pid;
  6. printf("before vfork\n");
  7. if ((pid = vfork()) < 0) err_sys("vfork err");
  8. else if (pid == 0)
  9. {
  10. globvar++, var++;
  11. _exit(0);
  12. }
  13. printf("pid = %u, globvar = %d, var = %d\n", pid, globvar, var);
  14. return 0;
  15. }

输出:

  1. before vfork
  2. pid = 3449, globvar = 7, var = 89

结果表明子进程修改了父进程的数据。

wait and waitpid

当一个子进程结束(不论是正常终止还是异常中止),内核会向父进程发送 SIGCHILD 信号。因为子进程中止是一个异步事件(这可以在父进程运行的任何时候发生),因此该信号也是内核向父进程发送的异步信号。

父进程接收到某一信号时,采取的措施可以是忽略,也可以是调用信号处理函数。对于 SIGCHILD 默认的措施是忽略。

  1. #include <sys/wait.h>
  2. pid_t wait(int *statloc);
  3. pid_t waitpid(pid_t pid, int *statloc, int options);
  4. // Both return: process ID if OK, 0 (see later), or −1 on error

作用:

  1. 如果所有子进程都在运行中,阻塞调用进程(即父进程)。
  2. 如果一个子进程已经结束,正等待父进程获取它的结束状态,则父进程取得子进程的中止状态后立即返回。
  3. 如果没有任何子进程,则返回 -1(通过 strerror(errno) 获取的错误信息为 No child processes)。

二者的区别:

  • 在一个子进程结束前,wait 使得父进程阻塞(只要有 1 个子进程结束,父进程就唤醒,返回值是刚刚结束的子进程的 pid );而 waitpid 可以通过参数设置,使得父进程不阻塞。
  • wait 可以选择等待某一进程 pid
  • waitpid(-1, &status, 0) 等价于 wait(&status) .

下面解析 3 个参数 pid, statloc, options .

statloc 用于获取子进程的结束状态,不同的比特位表示不同的含义,可以通过以下宏定义获取相关信息。

waitpidpid 的解释如下:

  • pid == -1: 等待任意一个子进程。
  • pid > 0 : 等待 pid 指定的进程。
  • pid == 0 : 等待 Group ID 等于调用进程组 ID 的任意一个子进程
  • pid < -1 : 等待 Group ID 等于 pid 绝对值的任意一个子进程。

options 可以为 0 ,或者以下常量的或运算 | 的结果:

例子 1

  1. #include <sys/wait.h>
  2. #include "apue.h"
  3. void pr_exit(int status)
  4. {
  5. if (WIFEXITED(status))
  6. printf("normal termination, exit status = %d\n", WEXITSTATUS(status));
  7. else if (WIFSIGNALED(status))
  8. printf("abnormal termination, signal number = %d%s\n",
  9. WTERMSIG(status),
  10. #ifdef WCOREDUMP
  11. WCOREDUMP(status) ? " (core file generated)" : "");
  12. #else
  13. "");
  14. #endif
  15. else if (WIFSTOPPED(status))
  16. printf("child stopped, signal number = %d\n", WSTOPSIG(status));
  17. }
  18. int main()
  19. {
  20. pid_t pid;
  21. int status;
  22. // case-1: childs exits with 7
  23. if ((pid = fork()) < 0) err_sys("fork err\n");
  24. else if (pid == 0) exit(7);
  25. if (wait(&status) != pid) err_sys("wait err\n");
  26. pr_exit(status);
  27. // case-2: child aborts
  28. if ((pid = fork()) < 0) err_sys("fork err\n");
  29. else if (pid == 0) abort();
  30. if (wait(&status) != pid) err_sys("wait err\n");
  31. pr_exit(status);
  32. // case-3: 0 as the divider in child
  33. if ((pid = fork()) < 0) err_sys("fork err\n");
  34. else if (pid == 0) status /= 0;
  35. if (wait(&status) != pid) err_sys("wait err\n");
  36. pr_exit(status);
  37. return 0;
  38. }

输出:

  1. normal termination, exit status = 7
  2. abnormal termination, signal number = 6 (core file generated)
  3. abnormal termination, signal number = 8 (core file generated)

例子 2 : 僵尸进程

  1. #include "apue.h"
  2. #include <sys/wait.h>
  3. int main()
  4. {
  5. pid_t pid;
  6. if ((pid = fork()) < 0) err_sys("fork err");
  7. else if (pid == 0)
  8. {
  9. if ((pid = fork()) < 0) err_sys("fork err");
  10. else if (pid > 0) exit(0);
  11. // child-2 continues when its parent exit
  12. // then child-2's parent will be init (pid=1)
  13. sleep(2);
  14. printf("second child, parent pid = %u\n", getppid());
  15. exit(0);
  16. }
  17. if (waitpid(pid, NULL, 0) != pid) err_sys("waitpid err");
  18. exit(0);
  19. }
  20. // Output: second child, parent pid = 1

waitid

  1. #include <sys/wait.h>
  2. int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
  3. // Returns: 0 if OK, −1 on error

waitidwaitpid 相比,具有更多的灵活性。

waitid 允许等待指定的某一子进程,但它使用 2 个单独的参数表示要等待的子进程的所属类型。

idtype 的含义如下:

options 是下列常量按位或运算的结果:

Race Condition

fork 之后不能保证父进程与子进程哪一个先执行,因此容易发生 Race Condition,解决竞争问题需要同步机制。

显然 wait 是一种同步操作,保证了父进程在子进程结束后才能运行。

反过来,如果子进程想等待父进程结束,可以通过轮询 (Polling)的方式:

  1. while (getppid() != 1)
  2. sleep(1);

子进程每隔 1 秒被唤醒,然后进行条件测试,满足条件后才能继续运行。但这种轮询方式浪费 CPU 的时间片,效率是极其低下的。

因此,多进程之间需要有某种形式的信号发送与接收方法,来实现多进程的同步。这些内容将在后面继续讨论。

exec

终于看到本章的重点内容了。

当进程调用 exec 函数,该进程的内容就被完全替换为指定的新程序,新程序从它的 main 开始执行。应当注意的是:exec 不会创建新的进程,所以调用前后的进程 ID 不会变,exec 只是用磁盘上的某一程序替换了当前的 text 段,数据段,堆和栈。

  1. #include <unistd.h>
  2. int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
  3. int execv(const char *pathname, char *const argv[]);
  4. int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */ );
  5. int execve(const char *pathname, char *const argv[], char *const envp[]);
  6. int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );
  7. int execvp(const char *filename, char *const argv[]);
  8. int fexecve(int fd, char *const argv[], char *const envp[]);
  9. // All seven return: −1 on error, no return on success

先说 pathnamefilename 的区别:

  • pathname 是相对于当前工作目录的路径;
  • filename: 如果包含 / 符号,就将其视为路径;否则在 PATH 环境变量包含的目录中查找。

如果 execlp, execvpfilename 指向的不是一个由 Linker 产生的二进制可执行文件,那么会认为 filename 指向的是一个 Shell 脚本,调用 /bin/sh 或者 /bin/bash 执行之。比如:

  1. // Content of file 'echo3': echo $1 $2 $3
  2. execlp("/home/sinkinben/workspace/apue/echo3", "echo3", "sin", "kin", "ben", NULL);
  3. // or
  4. char* argv[] = {"echo3", "sin", "kin", "ben", NULL};
  5. execvp("/home/sinkinben/workspace/apue/echo3", argv);

fexecve 根据调用者提供的 fd 来寻找可执行文件。调用者可以使用文件描述符验证所需要的的文件存在,并且无竞争地执行该文件。否则如果在调用 exec 前,pathname, filename 指向的可执行文件的内容被恶意篡改,容易引发安全漏洞。

第二个区别是参数列表的传递方式(函数名字的 l 表示 list, v 表示 vector)。

  • l 表示将调用的命令行参数通过一个单独的参数传递(如上面的 execlp ),最后带一个 NULL
  • v 表示命令行参数需要组合成一个数组的形式(如上面的 execvp)。

对于 execle, execve 允许通过 char *const envp[] 设置环境表(e表示 envp)。

此外,函数名还有一个 pexeclp, execvp ,其中 p 表示该函数以 filename 作为参数,可以在 PATH 中寻找可执行文件。

下图为 7 个 exec 函数的对比。

下图为 7 个 exec 的关系图。

对于 fexecve 而言,它会把 fd 参数转换为形如 /proc/{pid}/fd/{x} 的路径(该路径「指向」某一可执行文件)。

例子

  1. char *env_init[] = {"USER=unknown", "PATH=/tmp", NULL};
  2. int main()
  3. {
  4. pid_t pid;
  5. if ((pid = fork()) < 0) err_sys("fork err");
  6. else if (pid == 0)
  7. {
  8. if (execle("/tmp/echoall", "echoall", "arg1", "arg2", NULL, env_init) < 0)
  9. err_sys("execle err");
  10. }
  11. waitpid(pid, NULL, 0);
  12. exit(0);
  13. }

其中 echoall 是一个打印 argvenviron 的程序(编译后放在 /tmp 下):

  1. int main(int argc, char *argv[])
  2. {
  3. int i;
  4. extern char **environ;
  5. for (i = 0; i < argc; i++) printf("argv[%d] = %s\n", i, argv[i]);
  6. for (i = 0; environ[i] != NULL; i++) puts(environ[i]);
  7. }

运行结果:

  1. argv[0] = echoall
  2. argv[1] = arg1
  3. argv[2] = arg2
  4. USER=unknown
  5. PATH=/tmp

例子

最后来看个例子,如何实现 Shell 中的管道 | 功能。

  1. #include <unistd.h>
  2. #include <stdio.h>
  3. #include <errno.h>
  4. #include <string.h>
  5. int main()
  6. {
  7. // exec: lcmd | rcmd
  8. // e.g. cat pipe.c | wc -l
  9. char *lcmd[] = {"cat", "pipe.c", NULL};
  10. char *rcmd[] = {"head", "-n", "10", NULL};
  11. int fd[2];
  12. pipe(fd);
  13. pid_t pid;
  14. if ((pid = fork()) == 0)
  15. {
  16. dup2(fd[1], 1);
  17. close(fd[0]), close(fd[1]);
  18. execvp(lcmd[0], lcmd);
  19. // should not be here
  20. exit(-1);
  21. }
  22. else if (pid > 0)
  23. {
  24. waitpid(pid, NULL, 0);
  25. if ((pid = fork()) == 0)
  26. {
  27. dup2(fd[0], 0);
  28. close(fd[0]), close(fd[1]);
  29. execvp(rcmd[0], rcmd);
  30. // should not be here
  31. exit(-1);
  32. }
  33. else if (pid > 0)
  34. {
  35. close(fd[0]), close(fd[1]);
  36. waitpid(pid, NULL, 0);
  37. }
  38. }
  39. }

[APUE] 进程控制的更多相关文章

  1. [APUE]进程控制(上)

    一.进程标识 进程ID 0是调度进程,常常被称为交换进程(swapper).该进程并不执行任何磁盘上的程序--它是内核的一部分,因此也被称为系统进程.进程ID 1是init进程,在自举(bootstr ...

  2. [APUE]进程控制(中)

    一.wait和waitpid函数 当一个进程正常或异常终止时会向父进程发送SIGCHLD信号.对于这种信号系统默认会忽略.调用wait/waidpid的进程可能会: 阻塞(如果其子进程都还在运行); ...

  3. [APUE]进程控制(下)

    一.更改用户ID和组ID 可以用setuid设置实际用户ID和有效用户ID.可以用setgid函数设置实际组ID和有效组ID. #include <sys/types.h> #includ ...

  4. (六) 一起学 Unix 环境高级编程 (APUE) 之 进程控制

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  5. APUE(8)---进程控制(1)

    一.进程标识 每个进程都有一个非负整型标识的唯一进程ID.因为进程ID标识符总是唯一的,常将其用做其他标识符的一部分以保证其唯一性.进程ID虽然是唯一的, 但是却是可以复用的.ID为0的进程通常是调度 ...

  6. 进程控制(Note for apue and csapp)

    1. Introduction We now turn to the process control provided by the UNIX System. This includes the cr ...

  7. apue学习笔记(第八章 进程控制)

    本章介绍UNIX系统的进程控制,包括创建新进程.执行程序和进程终止. 进程标识 每一个进程都有一个非负整数表示的唯一进程ID,除了进程ID,每个进程还有一些其他标识符.下列函数返回这些标识符 #inc ...

  8. 《UNIX环境高级编程》(APUE) 笔记第八章 - 进程控制

    8 - 进程控制 Github 地址 1. 进程标识 每个进程都有一个非负整型表示的 唯一进程 ID .进程 ID 是可复用的(延迟复用算法). ID 为 \(0\) 的进程通常是调度进程,常常被称为 ...

  9. 进程控制之exec函数

    用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序.当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行.因为调用exec并不创 ...

随机推荐

  1. 【Oracle】查看表或视图的创建语句

    这里用到的是Oracle的DDL语句的用法 用于获得某个schema下所有的表.索引.视图.存储过程.函数的DDL set pagesize 0 set long 90000 set feedback ...

  2. Log4j日志记录

    1.导入log4j的jar包 2.写log4j.properties文件,配置日志记录参数,一般参数如下所示: 第二行指定了输出日志的目录,此处用的相对路径,也可换成绝对路径: 第三行指定了输出的记录 ...

  3. 攻防世界—pwn—int_overflow

    题目分析 checksec检查文件保护机制 ida分析程序 经典整数溢出漏洞示例 整数溢出原理整数分为有符号和无符号两种类型,有符号数以最高位作为其符号位,即正整数最高位为1,负数为0, 无符号数取值 ...

  4. CTFhub刷题记录

    一 [WesternCTF2018]shrine 没什么好说的,SSTI模版注入类问题,过滤了()但是我们不慌.开始注入,{{29*3}}测试通过. 发现是jinjia2的模版注入.关键点在于没有() ...

  5. pg_rman的安装与使用

    1.下载对应数据库版本及操作系统的pg_rman源码 https://github.com/ossc-db/pg_rman/releases 本例使用的是centos6.9+pg10,因此下载的是pg ...

  6. SwiftUI 中一些和响应式状态有关的属性包装器的用途

    SwiftUI 借鉴了 React 等 UI 框架的概念,通过 state 的变化,对 View 进行响应式的渲染.主要通过 @State, @StateObject, @ObservedObject ...

  7. SAP里会话结束方法(杀死进程)

    在SAP的ERP里,有很多方法可以结束一个会话,然而在不同情况下,需要使用的方法也不同.下面从先后顺序来简单说明:1.SM04:最常用的方法,在SM04点击工具栏的会话->结束会话,来关闭一个会 ...

  8. [Usaco2007 Jan]Telephone Lines架设电话线

    题目描述 FarmerJohn打算将电话线引到自己的农场,但电信公司并不打算为他提供免费服务.于是,FJ必须为此向电信公司支付一定的费用.FJ的农场周围分布着N(1<=N<=1,000)根 ...

  9. 15V转5V转3.3V转3V芯片,DC-DC和LDO

    15V电压是属于一般电压,降压转成5V电压,3.3V电压和3V电压,适用于这个电压的DC-DC很多,LDO也是有可以选择的.LDO芯片如PW6206,PW8600等.DC-DC芯片如:PW2162,P ...

  10. Flask扩展点总结(信号)

    信号(源码) 信号,是在flask框架中为我们预留的钩子,让我们可以进行一些自定义操作. pip3 install blinker 根据flask项目的请求流程来进行设置扩展点 1.中间件 from ...