[APUE] 进程控制
APUE 一书的第八章学习笔记。
进程标识
大家都知道使用 PID 来标识的。
系统中的一些特殊进程:
- PID = 0: 调度进程,也称为交换进程 (Swapper)
- PID = 1:
init
进程,自检结束后由内核调用,读取与系统初始化相关的文件,如/etc/init.d/*, /etc/rc*/
.init
进程是一个以root
启动的普通进程,而不是像 Swapper 是一个内核进程。init
是所有孤儿进程的父进程。 - PID = 2: 页守护进程 (Page Daemon), 为虚拟存储器的分页操作提供支持。
关于进程标识的 API :
#include <unistd.h>
pid_t getpid(void); // Returns: process ID of calling process
pid_t getppid(void); // Returns: parent process ID of calling process
uid_t getuid(void); // Returns: real user ID of calling process
uid_t geteuid(void); // Returns: effective user ID of calling process
gid_t getgid(void); // Returns: real group ID of calling process
gid_t getegid(void); // Returns: effective group ID of calling process
fork
#include <unistd.h>
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 种常见用法:
- 父进程复制自己,父子进程同时执行不同的代码段。这种情况常见于网络服务进程:父进程等待客户端的请求,当请求到达时,父进程调用
fork
,使子进程处理该请求,而父进程继续等待下一请求。 - 一个进程需要执行不同的程序。例如 Shell 程序,子进程从
fork
返回之后调用exec
系列函数。在某些系统中,会把fork, exec
封装为一种操作spawn
.
例子
#include "apue.h"
int globalvar = 123;
char buf[] = "a write to stdout\n";
int main()
{
int var = 233;
pid_t pid;
if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1) err_sys("write err\n");
if ((pid = fork()) < 0) err_sys("fork err\n");
else if (pid == 0) var++, globalvar++;
else sleep(2);
printf("pid=%d, globalvar=%d, var=%d\n", pid, globalvar, var);
return 0;
}
输出:
$ ./a.out
a write to stdout
pid=0, globalvar=124, var=234
pid=15438, globalvar=123, var=233
文件共享
fork
之后,子进程会拥有父进程的文件描述符表的副本,如下图所示。
所以:
- 父进程的重定向
dup
也会被子进程继承。 - 父子进程共享某一打开文件的偏移量。如果父子进程同时对该文件进行写操作(但没有任何同步机制),那么就会造成数据的混乱。
vfork
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
vfork
用于创建一个子进程,而该子进程的目的是执行 exec
系列函数。
vfork
并不会把父进程的地址空间完全复制到子进程中,因为考虑到子进程会马上调用 exec
(因而不会引用该地址空间的数据),不过在它调用 exec, exit
之前,它一直在父进程的地址空间中运行。但如果子进程后续没有调用 exec
或者 exit
,是一种未定义行为。
vfork
和 fork
的另外一个重要区别是:vfork
保证子进程先运行,在它调用 exec, exit
之后父进程才可能被调度运行(如果这 2 个调用依赖于父进程的进一步动作,那么会产生死锁)。
例子
int globvar = 6;
int main()
{
int var = 88;
pid_t pid;
printf("before vfork\n");
if ((pid = vfork()) < 0) err_sys("vfork err");
else if (pid == 0)
{
globvar++, var++;
_exit(0);
}
printf("pid = %u, globvar = %d, var = %d\n", pid, globvar, var);
return 0;
}
输出:
before vfork
pid = 3449, globvar = 7, var = 89
结果表明子进程修改了父进程的数据。
wait and waitpid
当一个子进程结束(不论是正常终止还是异常中止),内核会向父进程发送 SIGCHILD
信号。因为子进程中止是一个异步事件(这可以在父进程运行的任何时候发生),因此该信号也是内核向父进程发送的异步信号。
父进程接收到某一信号时,采取的措施可以是忽略,也可以是调用信号处理函数。对于 SIGCHILD
默认的措施是忽略。
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
// Both return: process ID if OK, 0 (see later), or −1 on error
作用:
- 如果所有子进程都在运行中,阻塞调用进程(即父进程)。
- 如果一个子进程已经结束,正等待父进程获取它的结束状态,则父进程取得子进程的中止状态后立即返回。
- 如果没有任何子进程,则返回 -1(通过
strerror(errno)
获取的错误信息为No child processes
)。
二者的区别:
- 在一个子进程结束前,
wait
使得父进程阻塞(只要有 1 个子进程结束,父进程就唤醒,返回值是刚刚结束的子进程的pid
);而waitpid
可以通过参数设置,使得父进程不阻塞。 wait
可以选择等待某一进程pid
。waitpid(-1, &status, 0)
等价于wait(&status)
.
下面解析 3 个参数 pid, statloc, options
.
statloc
用于获取子进程的结束状态,不同的比特位表示不同的含义,可以通过以下宏定义获取相关信息。
在 waitpid
中 pid
的解释如下:
pid == -1
: 等待任意一个子进程。pid > 0
: 等待pid
指定的进程。pid == 0
: 等待 Group ID 等于调用进程组 ID 的任意一个子进程。pid < -1
: 等待 Group ID 等于pid
绝对值的任意一个子进程。
options
可以为 0 ,或者以下常量的或运算 |
的结果:
例子 1
#include <sys/wait.h>
#include "apue.h"
void pr_exit(int status)
{
if (WIFEXITED(status))
printf("normal termination, exit status = %d\n", WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("abnormal termination, signal number = %d%s\n",
WTERMSIG(status),
#ifdef WCOREDUMP
WCOREDUMP(status) ? " (core file generated)" : "");
#else
"");
#endif
else if (WIFSTOPPED(status))
printf("child stopped, signal number = %d\n", WSTOPSIG(status));
}
int main()
{
pid_t pid;
int status;
// case-1: childs exits with 7
if ((pid = fork()) < 0) err_sys("fork err\n");
else if (pid == 0) exit(7);
if (wait(&status) != pid) err_sys("wait err\n");
pr_exit(status);
// case-2: child aborts
if ((pid = fork()) < 0) err_sys("fork err\n");
else if (pid == 0) abort();
if (wait(&status) != pid) err_sys("wait err\n");
pr_exit(status);
// case-3: 0 as the divider in child
if ((pid = fork()) < 0) err_sys("fork err\n");
else if (pid == 0) status /= 0;
if (wait(&status) != pid) err_sys("wait err\n");
pr_exit(status);
return 0;
}
输出:
normal termination, exit status = 7
abnormal termination, signal number = 6 (core file generated)
abnormal termination, signal number = 8 (core file generated)
例子 2 : 僵尸进程
#include "apue.h"
#include <sys/wait.h>
int main()
{
pid_t pid;
if ((pid = fork()) < 0) err_sys("fork err");
else if (pid == 0)
{
if ((pid = fork()) < 0) err_sys("fork err");
else if (pid > 0) exit(0);
// child-2 continues when its parent exit
// then child-2's parent will be init (pid=1)
sleep(2);
printf("second child, parent pid = %u\n", getppid());
exit(0);
}
if (waitpid(pid, NULL, 0) != pid) err_sys("waitpid err");
exit(0);
}
// Output: second child, parent pid = 1
waitid
#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
// Returns: 0 if OK, −1 on error
waitid
与 waitpid
相比,具有更多的灵活性。
waitid
允许等待指定的某一子进程,但它使用 2 个单独的参数表示要等待的子进程的所属类型。
idtype
的含义如下:
options
是下列常量按位或运算的结果:
Race Condition
fork
之后不能保证父进程与子进程哪一个先执行,因此容易发生 Race Condition,解决竞争问题需要同步机制。
显然 wait
是一种同步操作,保证了父进程在子进程结束后才能运行。
反过来,如果子进程想等待父进程结束,可以通过轮询 (Polling)的方式:
while (getppid() != 1)
sleep(1);
子进程每隔 1 秒被唤醒,然后进行条件测试,满足条件后才能继续运行。但这种轮询方式浪费 CPU 的时间片,效率是极其低下的。
因此,多进程之间需要有某种形式的信号发送与接收方法,来实现多进程的同步。这些内容将在后面继续讨论。
exec
终于看到本章的重点内容了。
当进程调用 exec
函数,该进程的内容就被完全替换为指定的新程序,新程序从它的 main
开始执行。应当注意的是:exec
不会创建新的进程,所以调用前后的进程 ID 不会变,exec
只是用磁盘上的某一程序替换了当前的 text 段,数据段,堆和栈。
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */ );
int execve(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 argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
// All seven return: −1 on error, no return on success
先说 pathname
与 filename
的区别:
pathname
是相对于当前工作目录的路径;filename
: 如果包含/
符号,就将其视为路径;否则在PATH
环境变量包含的目录中查找。
如果 execlp, execvp
的 filename
指向的不是一个由 Linker 产生的二进制可执行文件,那么会认为 filename
指向的是一个 Shell 脚本,调用 /bin/sh
或者 /bin/bash
执行之。比如:
// Content of file 'echo3': echo $1 $2 $3
execlp("/home/sinkinben/workspace/apue/echo3", "echo3", "sin", "kin", "ben", NULL);
// or
char* argv[] = {"echo3", "sin", "kin", "ben", NULL};
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
)。
此外,函数名还有一个 p
的 execlp, execvp
,其中 p
表示该函数以 filename
作为参数,可以在 PATH
中寻找可执行文件。
下图为 7 个 exec
函数的对比。
下图为 7 个 exec
的关系图。
对于 fexecve
而言,它会把 fd
参数转换为形如 /proc/{pid}/fd/{x}
的路径(该路径「指向」某一可执行文件)。
例子
char *env_init[] = {"USER=unknown", "PATH=/tmp", NULL};
int main()
{
pid_t pid;
if ((pid = fork()) < 0) err_sys("fork err");
else if (pid == 0)
{
if (execle("/tmp/echoall", "echoall", "arg1", "arg2", NULL, env_init) < 0)
err_sys("execle err");
}
waitpid(pid, NULL, 0);
exit(0);
}
其中 echoall
是一个打印 argv
和 environ
的程序(编译后放在 /tmp
下):
int main(int argc, char *argv[])
{
int i;
extern char **environ;
for (i = 0; i < argc; i++) printf("argv[%d] = %s\n", i, argv[i]);
for (i = 0; environ[i] != NULL; i++) puts(environ[i]);
}
运行结果:
argv[0] = echoall
argv[1] = arg1
argv[2] = arg2
USER=unknown
PATH=/tmp
例子
最后来看个例子,如何实现 Shell 中的管道 |
功能。
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
// exec: lcmd | rcmd
// e.g. cat pipe.c | wc -l
char *lcmd[] = {"cat", "pipe.c", NULL};
char *rcmd[] = {"head", "-n", "10", NULL};
int fd[2];
pipe(fd);
pid_t pid;
if ((pid = fork()) == 0)
{
dup2(fd[1], 1);
close(fd[0]), close(fd[1]);
execvp(lcmd[0], lcmd);
// should not be here
exit(-1);
}
else if (pid > 0)
{
waitpid(pid, NULL, 0);
if ((pid = fork()) == 0)
{
dup2(fd[0], 0);
close(fd[0]), close(fd[1]);
execvp(rcmd[0], rcmd);
// should not be here
exit(-1);
}
else if (pid > 0)
{
close(fd[0]), close(fd[1]);
waitpid(pid, NULL, 0);
}
}
}
[APUE] 进程控制的更多相关文章
- [APUE]进程控制(上)
一.进程标识 进程ID 0是调度进程,常常被称为交换进程(swapper).该进程并不执行任何磁盘上的程序--它是内核的一部分,因此也被称为系统进程.进程ID 1是init进程,在自举(bootstr ...
- [APUE]进程控制(中)
一.wait和waitpid函数 当一个进程正常或异常终止时会向父进程发送SIGCHLD信号.对于这种信号系统默认会忽略.调用wait/waidpid的进程可能会: 阻塞(如果其子进程都还在运行); ...
- [APUE]进程控制(下)
一.更改用户ID和组ID 可以用setuid设置实际用户ID和有效用户ID.可以用setgid函数设置实际组ID和有效组ID. #include <sys/types.h> #includ ...
- (六) 一起学 Unix 环境高级编程 (APUE) 之 进程控制
. . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...
- APUE(8)---进程控制(1)
一.进程标识 每个进程都有一个非负整型标识的唯一进程ID.因为进程ID标识符总是唯一的,常将其用做其他标识符的一部分以保证其唯一性.进程ID虽然是唯一的, 但是却是可以复用的.ID为0的进程通常是调度 ...
- 进程控制(Note for apue and csapp)
1. Introduction We now turn to the process control provided by the UNIX System. This includes the cr ...
- apue学习笔记(第八章 进程控制)
本章介绍UNIX系统的进程控制,包括创建新进程.执行程序和进程终止. 进程标识 每一个进程都有一个非负整数表示的唯一进程ID,除了进程ID,每个进程还有一些其他标识符.下列函数返回这些标识符 #inc ...
- 《UNIX环境高级编程》(APUE) 笔记第八章 - 进程控制
8 - 进程控制 Github 地址 1. 进程标识 每个进程都有一个非负整型表示的 唯一进程 ID .进程 ID 是可复用的(延迟复用算法). ID 为 \(0\) 的进程通常是调度进程,常常被称为 ...
- 进程控制之exec函数
用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序.当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行.因为调用exec并不创 ...
随机推荐
- 【Oracle】查看表或视图的创建语句
这里用到的是Oracle的DDL语句的用法 用于获得某个schema下所有的表.索引.视图.存储过程.函数的DDL set pagesize 0 set long 90000 set feedback ...
- Log4j日志记录
1.导入log4j的jar包 2.写log4j.properties文件,配置日志记录参数,一般参数如下所示: 第二行指定了输出日志的目录,此处用的相对路径,也可换成绝对路径: 第三行指定了输出的记录 ...
- 攻防世界—pwn—int_overflow
题目分析 checksec检查文件保护机制 ida分析程序 经典整数溢出漏洞示例 整数溢出原理整数分为有符号和无符号两种类型,有符号数以最高位作为其符号位,即正整数最高位为1,负数为0, 无符号数取值 ...
- CTFhub刷题记录
一 [WesternCTF2018]shrine 没什么好说的,SSTI模版注入类问题,过滤了()但是我们不慌.开始注入,{{29*3}}测试通过. 发现是jinjia2的模版注入.关键点在于没有() ...
- pg_rman的安装与使用
1.下载对应数据库版本及操作系统的pg_rman源码 https://github.com/ossc-db/pg_rman/releases 本例使用的是centos6.9+pg10,因此下载的是pg ...
- SwiftUI 中一些和响应式状态有关的属性包装器的用途
SwiftUI 借鉴了 React 等 UI 框架的概念,通过 state 的变化,对 View 进行响应式的渲染.主要通过 @State, @StateObject, @ObservedObject ...
- SAP里会话结束方法(杀死进程)
在SAP的ERP里,有很多方法可以结束一个会话,然而在不同情况下,需要使用的方法也不同.下面从先后顺序来简单说明:1.SM04:最常用的方法,在SM04点击工具栏的会话->结束会话,来关闭一个会 ...
- [Usaco2007 Jan]Telephone Lines架设电话线
题目描述 FarmerJohn打算将电话线引到自己的农场,但电信公司并不打算为他提供免费服务.于是,FJ必须为此向电信公司支付一定的费用.FJ的农场周围分布着N(1<=N<=1,000)根 ...
- 15V转5V转3.3V转3V芯片,DC-DC和LDO
15V电压是属于一般电压,降压转成5V电压,3.3V电压和3V电压,适用于这个电压的DC-DC很多,LDO也是有可以选择的.LDO芯片如PW6206,PW8600等.DC-DC芯片如:PW2162,P ...
- Flask扩展点总结(信号)
信号(源码) 信号,是在flask框架中为我们预留的钩子,让我们可以进行一些自定义操作. pip3 install blinker 根据flask项目的请求流程来进行设置扩展点 1.中间件 from ...