早,几天不见,是否有人想念我。
你是在进行无效阅读吗?
1. 什么是无效阅读:
每天都读十几篇干货满满的文章,但读完也就忘了,记不住也学不到,这就是无效阅读。
无效阅读的目标不是要通过阅读解决一个什么问题、学习一个什么技巧、思考一个什么话题。一篇文章或者一本书读完了,任务也就完成了,这样的阅读只能起到自我安慰的作用。
很多人把阅读目标定为 “我这周要读完2本书”,“我今天要看完10篇文章”,其实根本没用,因为读完不是目的,吸收才是目的。
2. 几个提高阅读效果的技巧:
要带着目标和预期阅读;
边阅读边思考,创造环境去做笔记,善用手机,善用微信。
进行主题式聚焦阅读;
好内容要反复阅读;
带着批判性思维和学习的心态阅读;
阅读时,不断代入自己的工作和生活场景。
以下是正文,先明确正文目的:
举例说明在 Linux 中如何使用 pipe 进行父子进程间的同步。
目录:
一、基础知识
二、使用管道实现相关进程间的同步
1. 以不交互的方式实现同步
2. 以交互的方式实现同步
三、参考书籍
四、欢迎加入我的微信群
一、基础知识
1. 关于竞争
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,我们认为发生了 竞争条件 (race condition)。如果在 fork 之后的某种逻辑显式或隐式地依赖于在 fork 之后是父进程先运行还是子进程先运行,那么 fork 函数就会是竞争条件活跃的滋生地。通常,我们不能预料哪一个进程先运行。即使我们知道哪一个进程先运行,在该进程开始运行后所发生的事情也依赖于系统负载以及内核的调度算法。
演示 demo (tellwait1.c):
static void charatatime(char *);
int main(void)
{
pid_t pid;
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
charatatime("output from child\n");
} else {
charatatime("output from parent\n");
}
exit(0);
}
static void
charatatime(char *str)
{
char *ptr;
int c;
/* set unbuffered */
setbuf(stdout, NULL);
for (ptr = str; (c = *ptr++) != 0; )
putc(c, stdout);
}
运行效果:
$ ./tellwait1
output from parento
utput from child
$ ./tellwait1
output from parent
output from child
相关要点:
将标准输出设置为不带缓冲的,从而让每个字符输出都需调用一次 write()。目的是使内核能尽可能多次地在两个进程之间进行切换,以便演示竞争条件。
为了避免竞争条件和轮询,在多个进程之间需要有某种传递信息的方法,这种做法就叫做同步。在 UNIX 中可以使用 信号机制 和 各种进程间通信 (IPC) 来达到此目的
演示 demo (tellwait2.c):
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) {
/* parent goes first */
WAIT_PARENT();
charatatime("output from child\n");
} else {
charatatime("output from parent\n");
TELL_CHILD(pid);
}
2. 用于同步的工具 (Synchronization Facilities)
计算机科学中,同步 ( synchronization ) 是指两个不同但有联系的概念:进程同步 与 数据同步。
进程同步指多个进程在特定点会合 ( join up ) 或者握手使得达成协议或者使得操作序列有序。数据同步指一个数据集的多份拷贝一致以维护完整性。
常用进程同步实现数据同步。
同步工具可以协调进程的操作。通过同步可以防止进程执行诸如同时更新一块共享内存或同时更新文件的同一个数据块之类的操作。如果没有同步,同时更新的操作可能会导致应用程序产生错误的结果。
UNIX 系统提供了下列同步工具:
信号量 ( Semaphores ):
一个信号量是一个由内核维护的整数,其值永远不会小于0。一个进程可以增加或减小一个信号量的值。如果一个进程试图将信号量的值减小到小于0,那么内核会阻塞该操作直至信号量的值增长到允许执行该操作的程度。
Linux 既提供了 System V 信号量,又提供了 POSIX 信号量,它们的功能类似。
文件锁 ( File locks ):
文件锁是设计用来协调操作同一文件的多个进程的动作的一种同步方法。
Linux 通过 flock() 和 fcntl() 来提供文件加锁功能。
互斥体和条件变量 (Mutexes and condition):
通常用于 POSIX 线程。
通信工具 ( Communication facilities ):
通信工具也可以用来进行同步,例如信号、管道。
一般来讲,所有数据传输工具都可以用来同步,只是同步操作是通过在工具中交换消息来完成的。
在执行进程间同步时通常需要根据功能需求来选择工具。当协调对文件的访问时文件记录加锁通常是最佳的选择,而对于协调对其他共享资源的访问来讲,信号量通常是更佳的选择。
二、使用管道实现相关进程间的同步
1. 以不交互的方式实现同步
演示 demo (pipe_sync.c):
int main(int argc, char *argv[])
{
int pfd[2];
int j, dummy;
...
setbuf(stdout, NULL);
printf("%s Parent started\n", currTime("%T"));
if (pipe(pfd) == -1)
errExit("pipe");
for (j = 1; j < argc; j++) {
switch (fork()) {
case -1:
errExit("fork %d", j);
case 0: /* Child */
if (close(pfd[0]) == -1) /* Read end is unused */
errExit("close");
/* Child does some work, and lets parent know it's done */
sleep(getInt(argv[j], GN_NONNEG, "sleep-time"));
printf("%s Child %d (PID=%ld) closing pipe\n",
currTime("%T"), j, (long) getpid());
if (close(pfd[1]) == -1)
errExit("close");
_exit(EXIT_SUCCESS);
default: /* Parent loops to create next child */
break;
}
}
if (close(pfd[1]) == -1) /* Write end is unused */
errExit("close");
/* Parent synchronizes with children */
if (read(pfd[0], &dummy, 1) != 0)
fatal("parent didn't get EOF");
printf("%s Parent ready to go\n", currTime("%T"));
exit(EXIT_SUCCESS);
}
运行效果:
$ ./pipe_sync 2 4 6
08:16:32 Parent started
08:16:34 Child 1 (PID=23166) closing pipe
08:16:36 Child 2 (PID=23167) closing pipe
08:16:38 Child 3 (PID=23168) closing pipe
08:16:38 Parent ready to go
程序分析:
这个程序创建了多个子进程(每个命令行参数对应一个子进程),每个子进程都完成某个动作,在本例中则是睡眠一段时间。父进程等待直到所有子进程完成了自己的动作为止。
父进程在创建子进程之前构建了一个管道。每个子进程会继承管道的写入端的文件描述符并在完成动作之后关闭这些描述符。当所有子进程都关闭了管道的写入端的文件描述符之后,父进程在管道上的 read() 就会结束并返回文件结束 EOF。这时,父进程就能够做其他工作了。
相关要点:
在父进程中关闭管道的未使用写入端对于这项技术的正常运转是至关重要的,否则父进程在试图从管道中读取数据时会被永远阻塞。
管道同步有一个优势: 它可以同来协调一个进程的动作使之与多个相关进程匹配。而多个标准信号无法排队的事实使得信号不适用于这种情形。
对这项技术进行扩展: 不关闭管道,每个子进程向管道写入一条包含其进程 ID 和状态信息的消息。或者每个子进程可以向管道写入一个字节。父进程可以计数和分析这些消息。这种方法考虑到了子进程意外终止而不是显式地关闭管道的情形。
2. 以交互的方式实现同步
演示 demo (tellwait.c):
static int pfd1[2], pfd2[2];
void TELL_WAIT(void)
{
if (pipe(pfd1) < 0 || pipe(pfd2) < 0)
err_sys("pipe error");
}
void TELL_PARENT(pid_t pid)
{
if (write(pfd2[1], "c", 1) != 1)
err_sys("write error");
}
void WAIT_PARENT(void)
{
char c;
if (read(pfd1[0], &c, 1) != 1)
err_sys("read error");
if (c != 'p')
err_quit("WAIT_PARENT: incorrect data");
}
void TELL_CHILD(pid_t pid)
{
if (write(pfd1[1], "p", 1) != 1)
err_sys("write error");
}
void WAIT_CHILD(void)
{
char c;
if (read(pfd2[0], &c, 1) != 1)
err_sys("read error");
if (c != 'c')
err_quit("WAIT_CHILD: incorrect data");
}
运行效果:
$ ./proc/tellwait2
output from parent
output from child
程序分析:
在调用fork之前创建了两个管道(pfd1 和 pfd2)。
父进程在调用 TELL_CHILD 时,向第一个管道 pfd1 写一个字符 “p”,子进程在调用 TELL_PARENT 时,向第二个管道写一个字符 “c”。相应的 WAIT_XXX 函数调用 read() 读一个字符,没有读到字符时则阻塞,即休眠等待。
示意图:
三、参考书籍
《UNIX 环境高级编程》
8.9 竞争条件
15.2 管道
《Linux / UNIX 系统编程手册》
43.进程间通信简介
44.管道和FIFO
《Linux 程序设计 - 13 进程间通信:管道》
《UNIX-Linux编程实践教程 - 10 I/O 重定向和管道》
《UNIX操作系统设计 - 5.12 管道》
《Linux内核设计的艺术图解 - 8.1 管道机制》
更多值得学习的知识点:
将管道作为一种进程同步的方法 (done);
使用管道连接过滤器;
通过管道与 shell 命令进行通信:popen();
管道和 stdio 缓冲;
管道的内核实现;
命名管道:FIFO;
鉴于大多数人的注意力无法在一篇文章里上集中太久,更多的内容请大家先自行去阅读吧,不是自己理解到的东西是消化不了的。有机会的话我会把更多的读书心得放在后面的文章。
四、欢迎加入我的微信群
你和我各有一个苹果,如果我们交换苹果的话,我们还是只有一个苹果。但当你和我各有一个想法,我们交换想法的话,我们就都有两个想法了。如果你也对 嵌入式系统和开源软件 感兴趣,并且想和更多人互相交流学习的话,请关注我的公众号:嵌入式Hacker,一起来学习吧,无论是 关注或转发 , 还是赏赐,都是对作者莫大的支持,感谢 各位的大拇指 ,祝工作顺利,家庭和睦~