b97902HW 板


LINE

简介 fork, exec*, pipe, dup2 前言 郑卜壬老师在上课的时候,以极快的速度讲过了 fork, exec*, pipe, dup2 几个指令,并以下面的程式示范了一个 Shell 如何实作 Redirection。我自己觉得这一部分很有趣,所以 花了一些时间去做一些实验,以下就把我的心得和大家分享。 Redirection 我们先从老师课堂上的那一个 Redirection 程式谈起。 /* 程式码 red.c */ #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> int main(int argc, char **argv) { /* -- Check Arguments -------- */ if (argc < 4) { fprintf(stderr, "Error: Incorrect Arguments.\n"); fprintf(stderr, "Usage: red STDIN_FILE STDOUT_FILE EXECUTABLE ARGS\n"); exit(EXIT_FAILURE); } /* -- Open the Files for Redirection -------- */ int fd_in = open(argv[1], O_RDONLY); if (fd_in < 0) { fprintf(stderr, "Error: Unable to open the input file.\n"); exit(EXIT_FAILURE); } else { dup2(fd_in, STDIN_FILENO); close(fd_in); } int fd_out = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd_out < 0) { fprintf(stderr, "Error: Unable to open the output file.\n"); exit(EXIT_FAILURE); } else { dup2(fd_out, STDOUT_FILENO); close(fd_out); } /* -- Replace the Executable Image of the Process -------- */ if (execvp(argv[3], argv + 3) == -1) { fprintf(stderr, "Error: Unable to load executable image.\n"); exit(EXIT_FAILURE); } return EXIT_SUCCESS; } 大家可以用 gcc red.c -o red 来编译这一个程式,然後用 ./red 标准输入 标准输出 可执行档 其他参数 来执行他。例如: touch empty.txt ./red empty.txt output.txt /bin/echo "Hello, redirection." 我们可以发现 /bin/echo 执行时,其标准输出被我们重新导向到 output.txt,而不是直接显示到萤幕上。接下来我们来细看 red 这一个程式。我们先去除所有的错误检查: int main(int argc, char **argv) { int fd_in = open(argv[1], O_RDONLY); /* 很明显地,这一行是要开启一个档案做为标准输入的来源。 fd_in = 3 fd[0] --------> file_table[i] (inherit from shell) fd[1] --------> file_table[j] (inherit from shell) fd[2] --------> file_table[k] (inherit from shell) fd[3] --------> file_table[m] (open argv[1]) */ dup2(fd_in, STDIN_FILENO); /* 接着我们会先关掉原本由 bash 之类的 Shell 自动帮我们开启的 STDIN。 这里的 STDIN_FILENO 事实上就是被 define 成 0。 然後,把第 fd_in 个 File descriptor 复制一份到第 0 个 File descriptor。 这时,同时会有二个 File descriptor 指向同一个档案(Kernel File Table 同一个 entry)。 fd[0] ----------------------------+ fd[1] --------> file_table[j] | fd[2] --------> file_table[k] | fd[3] --------> file_table[m] <---+ */ close(fd_in); /* 关闭第 fd_in 个 File descriptor。或者精确地说,回收第 fd_in 个 File descriptor。因为我们不需要这个 File descriptor 了。此时, 就只剩一个 File descriptor 指向 file_table[m]。 fd[0] --------> file_table[m] (open argv[1]) fd[1] --------> file_table[j] (inherit from shell) fd[2] --------> file_table[k] */ int fd_out = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644); /* 开启一个档案准备做为标准输出。如果档案不存在,就自动建立他,并将 其属性设为 644。若档案已经存在,记得要清空整个档案。 fd_out = 3 fd[0] --------> file_table[m] (open argv[1]) fd[1] --------> file_table[j] (inherit from shell) fd[2] --------> file_table[k] fd[3] --------> file_table[n] (open argv[2]) */ dup2(fd_out, STDOUT_FILENO); /* 和 STDIN 一样,我们关闭原有的 STDOUT,把我们开启的 fd_out 复制过去。 fd[0] --------> file_table[m] (open argv[1]) fd[1] --------------------------------+ fd[2] --------> file_table[k] | fd[3] --------> file_table[n] <-------+ */ close(fd_out); /* 和 STDIN 一样,把 fd_out 回收掉。 fd[0] --------> file_table[m] (open argv[1]) fd[1] --------> file_table[n] (open argv[2]) fd[2] --------> file_table[k] (inherit from shell) */ execvp(argv[3], argv + 3); /* 重新载入另一个执行档。我们下面会再谈到他。所有的 File descriptor 都 不会被改动(注1)。而 argv[3] 是可执行档的位置,argv + 3 是因为除了前 三个 argument 之外,我要把剩下的 argument 传给另一个执行档的 main() */ } 注1: 对於没有设定 FD_CLOEXEC 的 File descriptor,File descriptor 就不会因为 exec* 函式而被更动。本例之中,所有的 File descriptor 都没有设 FD_CLOEXEC。 exec* 重新载入可执行档 所谓的可执行档(Executable),就是我们在 Windows 上常见的 .exe, 或者是 Linux 上的 elf executable。这种档案会储存许多 instruction 的 machine code。而 exec* 系统呼叫就是帮我们把可执行档的 machine code 搬进 Process 的 Memory,然後呼叫可执行档之中的 main 函式。所以 exec* 系统呼叫的第一个参数通常都是可执行档的 位置。 另外,直得注意的是:如果 exec* 系统呼叫成功地被执行,这个 process 就好像一个人完全失忆,然後大脑被载入不同的记忆,他会完全忘记他本 来要执行的指令,从新的 main 函式开始。不过应该属於他的东西还会是 他的,例如:Process ID、File descriptor 等等。 想像一下,如果今天 LxxxxCxxxx 失忆了,然後有一个很强的催眠师 透过一些暗示,让 LxxxxCxxxx 有外星人的记忆。於是我就完全忘了 我是地球人,也忘了我写过这一篇教学文,也忘了我等一下要去睡觉。 我说起话来像是外星人,我的动作看起来像是外星人,事实上,我就 是外星人。不过我身边的东西,例如身分证上的身分证字号还是一样, 我手边的书还是 Alho 的 Compiler,不会因为很强的催眠师而改变。 如果今天,有一个 process 本来他的记忆是 a.out,不过不久之後 被 exec* 系统呼叫载入 /bin/ls。於是这个 process 完全忘了他 是 a.out,忘了他曾经呼叫过 exec*。他的输出结果像是 /bin/ls, 他的执行步骤看起来像是 /bin/ls,事实上他就是 /bin/ls。不过 这个 process 有的东西:Process ID、File Descriptor 不会因为 exec 而改变。 所以上面的 red.c 在 exec* 载入程式之後,如果直接把 fd[1] 当作 stdout,则所有的标准输出就会被写到我们开启的档案。 XD 还有,眼尖的同学可能已经注意到了!我上面所有的 exec 都加上了 *, 而在我的程式码之中,我呼叫的是 execv 的函式。事实上,为了方便 programmer 使用,exec* 有很多变体,如 execl, execv, execle, execve, execlp, execvp, execlp。他们的差异是: 结尾有 l 的,就是函式长度不固定(va_arg)的版本。这方便我们在 程式之中直接呼叫 exec,例如: execl("/bin/echo", "echo", "abc", "def", "ghi", (char *)0); 注意,一定要有一个 0 做为 argument 的结尾。如果是用以上的系 统呼叫,argv[0] 会是 "echo",argv[1] 会是 "abc",等等,而 argc 会是 4。 结尾有 v 的,就是 char ** 的版本。一样地,char ** 的最後一个 参数要以 0 做为结尾。 char *argvs[] = { "echo", "abc", "def", "ghi", 0 }; execv("/bin/echo", argvs); 结尾有 e 的就是可以设定 environment variable 的版本。例如: extern char **environ; int main() { execle("/bin/echo", "echo", "abc", "def", "ghi", (char *)0, environ); } 结尾有 p 的就是会自动去从 $PATH 找出 executable 的版本。 /*file*/ execlp("echo", "echo", "abc", "def", "ghi", (char *)0); execlp("ls", "ls", "-al", (char *)0); ref. http://www.opengroup.org/onlinepubs/000095399/functions/exec.html fork 建立一个 Child Process 看完上面的 exec 你可能会想:不对呀,我不过是想要呼叫其他的 程式,怎麽我就因此被取代了? 这时 fork 就要派上用场了! fork 这一个函式的功用就是完整地复制一份 Process,不论是他们的 Calling stack、Register、File descriptor 等等全部都复制一份, 然後回传 Process ID。这里,我们说完整的意思是这二个 Process 的执行的状态(Global Variable、Local Variable、Function Call 等等)几乎可以直接互换。而 File Descriptor 也会被 dup 一份。 唯一的不同就是呼叫 fork 的 Process,也就是正本,其 fork 函式 的回传值会是复本的 Process ID。而复本的 fork 的回传值一定是 0。 (如果没有错误产生的话) 我们可以把 fork 想像成一个很强大的人类复制机。今天 LoganChien 可以 fork 出一个 LxxxxCxxxx,他们二个不论是 DNA、记忆、外型都 一样。甚至包含按下复制前一刻的记忆都一样。唯一的不同就是我知 道复制人是 LxxxxCxxxx,而 LxxxxCxxxx 只知道他是一个复制人,不 知道他是由 LoganChien 复制出来的。 为什麽要这样设计?答案很简单!因为不这样设计,LxxxxCxxxx 不就 造反了?LoganChien 说 LxxxxCxxxx 是他的复制人,LxxxxCxxxx 说 LoganChien 是他的复制人。 而作业系统中的 Process 也是这样。我们 fork 之後,会产生二个 Process。基本上这二个 Process 是很像是,除了 Process ID 与 fork 的回传值,基本上这二个 Process 是一样的!所以为了让 Parent Process 可以掌控 Child Process,所以 Parent Process 的 fork 回传值是 Child Process 的 Process ID。而 Child Process 只会得到 0。 这样设计还有另一个用意,我们可以再同一份程式码处理 Parent Process 与 Child Process 的情况。我们通常是这样写 fork 的函式呼叫的: /* 程式码: fork.c */ #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> /* Required for wait() */ #include <unistd.h> int main() { pid_t proc = fork(); /* Create Child Process */ if (proc < 0) /* Failed to Create Child Process */ { printf("Error: Unable to fork.\n"); exit(EXIT_FAILURE); } else if (proc == 0) /* In the Child Process */ { printf("In the child process.\n"); exit(25); } else /* In the Parent Process */ { printf("In the parent process.\n"); int status = -1; wait(&status); /* Wait for Child Process */ printf("The Child Process Returned with %d\n", WEXITSTATUS(status)); } return EXIT_SUCCESS; } 我们会先用 fork 建立一个 Child Process。之後二个 Process 就 是各跑各的,除非 Parent Process 用 wait 来等待 Child Process 执行完毕。当然,照 POSIX API 的惯例,fork() 回传负的值,就代 表有错误发生。 在我们有了 fork 与 exec* 之後,我们就可以执行、呼叫另一个可 执行档了!原理很简单,我们只要在 Child Process 呼叫 exec* 就没有问题了。范例如下: #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> int main() { printf("Let's invoke ls command!\n\n"); pid_t proc = fork(); if (proc < 0) { printf("Error: Unable to fork.\n"); exit(EXIT_FAILURE); } else if (proc == 0) { if (execlp("ls", "ls", "-al", (char *)0) == -1) { printf("Error: Unable to load the executable.\n"); exit(EXIT_FAILURE); } /* NEVER REACHED */ } else { int status = -1; wait(&status); printf("\nThe exit code of ls is %d.\n", WEXITSTATUS(status)); } return EXIT_SUCCESS; } (未完,待续) -- LoganChien ----- from PTT2 个板 logan ----- --



※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 140.112.247.159
1F:推 integritywei:头推 03/19 01:18
2F:推 bombom:额头推 03/19 01:22
3F:推 avogau:眼睛推 03/19 01:49
4F:推 iownthegame:以极快的速度推文 03/19 01:55
5F:推 xflash96:大推。 03/19 07:53
6F:推 pj2:We will discuss pipe, fork and exec in details later. 03/19 09:26
7F:→ pj2:So far, you should know why and how to use dup2 03/19 09:27
8F:→ pj2:Thank 子翔 for the complement 03/19 09:30
9F:推 hrs113355:推 03/19 17:18
10F:推 Daniel1147:推 03/19 20:45
11F:推 dennis2030:今天终於静下心来看完了 很实用! 03/26 00:05
12F:推 averangeall:太完美的教学文了!!!!!!!!!!!!! 04/18 16:40
13F:推 Bingojkt:教学文全消推1@w< 04/19 18:15
14F:推 happy8155: 朝圣推~~ 10/15 17:30
15F:推 stitch8467: b02首推 11/07 00:04







like.gif 您可能会有兴趣的文章
icon.png[问题/行为] 猫晚上进房间会不会有憋尿问题
icon.pngRe: [闲聊] 选了错误的女孩成为魔法少女 XDDDDDDDDDD
icon.png[正妹] 瑞典 一张
icon.png[心得] EMS高领长版毛衣.墨小楼MC1002
icon.png[分享] 丹龙隔热纸GE55+33+22
icon.png[问题] 清洗洗衣机
icon.png[寻物] 窗台下的空间
icon.png[闲聊] 双极の女神1 木魔爵
icon.png[售车] 新竹 1997 march 1297cc 白色 四门
icon.png[讨论] 能从照片感受到摄影者心情吗
icon.png[狂贺] 贺贺贺贺 贺!岛村卯月!总选举NO.1
icon.png[难过] 羡慕白皮肤的女生
icon.png阅读文章
icon.png[黑特]
icon.png[问题] SBK S1安装於安全帽位置
icon.png[分享] 旧woo100绝版开箱!!
icon.pngRe: [无言] 关於小包卫生纸
icon.png[开箱] E5-2683V3 RX480Strix 快睿C1 简单测试
icon.png[心得] 苍の海贼龙 地狱 执行者16PT
icon.png[售车] 1999年Virage iO 1.8EXi
icon.png[心得] 挑战33 LV10 狮子座pt solo
icon.png[闲聊] 手把手教你不被桶之新手主购教学
icon.png[分享] Civic Type R 量产版官方照无预警流出
icon.png[售车] Golf 4 2.0 银色 自排
icon.png[出售] Graco提篮汽座(有底座)2000元诚可议
icon.png[问题] 请问补牙材质掉了还能再补吗?(台中半年内
icon.png[问题] 44th 单曲 生写竟然都给重复的啊啊!
icon.png[心得] 华南红卡/icash 核卡
icon.png[问题] 拔牙矫正这样正常吗
icon.png[赠送] 老莫高业 初业 102年版
icon.png[情报] 三大行动支付 本季掀战火
icon.png[宝宝] 博客来Amos水蜡笔5/1特价五折
icon.pngRe: [心得] 新鲜人一些面试分享
icon.png[心得] 苍の海贼龙 地狱 麒麟25PT
icon.pngRe: [闲聊] (君の名は。雷慎入) 君名二创漫画翻译
icon.pngRe: [闲聊] OGN中场影片:失踪人口局 (英文字幕)
icon.png[问题] 台湾大哥大4G讯号差
icon.png[出售] [全国]全新千寻侘草LED灯, 水草

请输入看板名称,例如:Boy-Girl站内搜寻

TOP