输入输出重定向
在命令行运行程序时,可以用“>”运算符把标准输出
重定向到文件:
命令:python ./rssgossip.py Snooki > stories.txt
标准输出是三大默认数据流之一。顾名思义,数据流就
是流动的数据,数据从一个进程流出,然后流入另一个
进程。除了标准输入、标准输出和标准错误,还有其他
形式的数据流,例如文件连接和网络连接也属于数据流。
重定向进程的输出,相当于改变进程发送数据的方向。
原来标准输出会把数据发送到屏幕,现在可以让它把数
据发送到文件。
在命令行中,重定向是非常有用的命令。
但有没有办法
让进程重定向自己呢?
进程内部一瞥
进程含有它正在运行的程序,还有栈和堆数据空间。除此之
外,进程还需要记录数据流的连向,比如标准输出连到了哪
里。进程用文件描述符表示数据流,所谓的描述符其实就是
一个数字。进程会把文件描述符和对应的数据流保存在描述符
表中。
文件描述符是一个数字,它代表一条数据流。
描述符表的前三项万年不变:0号标准输入,1号标准输出,2
号标准错误。其他项要么为空,要么连接进程打开的数据流。
比如程序在打开文件进行读写时,就会占用其中一项。
创建进程以后,标准输入连到键盘,标准输出和标准错误连
到屏幕。它们会保持这样的连接,直到有人把它们重定向到
了其他地方。
重定向即替换数据流
如果想重定向标准输出,只需要修改表
中1号描述符对应的数据流就行了,标准输入标准错误等流同理。
所有向标准输出发送数据的函数会先查看描述符表,
看1号描述符指向哪条数据流,然后再把数据写到这
条数据流中 ,printf() 便是如此。只要修改描述符表,进程也能
重定向它们自己。
难怪要用“2>”
你可以在命令行用“>”运算符重定向标准输
出,用“2>”重定向标准错误:
实例:从 rssgossip 读取新闻链接
管道两侧的命令是父子关系
捕捉信号然后运行自己的代码
代码:
命令:./myprog > output.txt 2> errors.log
现在,知道为什么标准错误要用“2 >”来
重定向了吧,因为2 是标准错误在描述符
表中的编号 。在很多操作系统中,也可
以用“1 >”来重定向标准输出。而在类
Unix操作系统中,可以用以下命令把标
准错误和标准输出重定向到一个地方:
命令: ./myprog 2>&1
- // “2>”表示“重定向标准错误”。 &1”表示“到标准输出"
fileno()返回描述符号
每打开一个文件,操作系统都会在描述符表中新注册一项。
假设你打开了某个文件:
FILE *my_file = fopen("guitar.mp3", "r");
操作系统会打开guitar.mp3文件,然后返回一个指向它的
指针,操作系统还会遍历
描述符表寻找空项,把新文件注
册在其中,如下图的3
那么如何根据文件指针知道它是几号描述符呢?答案是
调用 fileno() 函数:
int descriptor = fileno(my_file);
在失败时不返回-1的函数很少, fileno() 就是其中之一。只
要你把打开文件的指针传给 fileno() ,它就一定会返回描述符
编号。
dup2()复制数据流
每次打开文件都会使用描述符表中新的一项。但如果你想修
改某个已经注册过的数据流,比如想让3号描述符重新指向
其他数据流,该怎么做?
可以用 dup2() 函数, dup2() 可以复
制数据流。假设你在3号描述符中注册了log.txt文件指针,
下面这行代码就能同时把它连接到2号描述符:
dup2(3, 2); // 把2重定向成了和3一样
错误处理代码的抽取
下面两段代码一看头就大:
pid_t pid = fork();
if (pid == -1) {
fprintf(stderr, " 无法克隆进程: %s\n", strerror(errno));
return 1;
}
if (execle(...) == -1) {
fprintf(stderr, " 无法运行脚本: %s\n", strerror(errno));
return 1;
}
首先, 需要把处理代码放到一个单独的error()函数中,然后把return语句换成exit()系统调用。
#include <stdlib.h> //为了使用exit系统调用,必须包含stdlib.h头文件
void error(char *msg)
{
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(1); //此句会立刻终止程序,并把退出状态置1。
}
现在就可以把那些烦人的错误检查代码换成:
pid_t pid = fork();
if (pid == -1) {
error(" 无法克隆进程 ");
}
if (execle(...) == -1) {
error(" 无法运行脚本 ");
}
注意:exit()系统调用是结束程序的最快方式。完全不用操心怎么返回主函数,直接调用exit(),你
的程序就会灰飞烟灭!
运行结果:
rssgossip.py 确实把数据发往标准输出时,数据应该出现在 stories.txt 文件中。但有时又是空的.这是为何?
有时需要等待......
ewshound2 程序启用独立的进程运行rssgossip.py脚本,而子进程一创建就和父进程没关系了。rssgossip.py还没有完成任务, newshound2 程序就结束了,所以stories.txt还是空的。也就是说,操作系统必须提供一种方式,让你等待子进程完成任务。
waitpid() 函数
waitpid() 函数会等子进程结束以后才返回,也就是说可以在程序中加几行代码,让它等rssgossip.py脚本运行结束以后才退出。
新代码加到newshound2程序底部:
#include <sys/wait.h>
...
int pid_status; //这个变量用来保存进程信息
if (waitpid(pid, &pid_status, 0) == -1) {
error(" 等待子进程时发生了错误 ");
}
return 0;
waitpid()的三个参数聚焦
1.Pid:父进程在克隆子进程时会得到子进程的ID。
2.pid_status: 用来保存进程的退出信息。因为 waitpid() 需要修改pid_status ,因此它必须是个指针。
3.选项: waitpid() 有一些选项,详情可以输入 man waitpid查看, 若把选项设为 0 ,函数将等待进程结束。
什么是 pid_status ?
waitpid() 函数结束等待时会在 pid_status 中保存一个值,它告诉你进程的完成情况。为了得到子进程的退出状态,可以把 pid_status 的值传给 WEXITSTATUS() 宏:
if (WEXITSTATUS(pid_status)) //如果退出状态不是 0
Puts("Error status non-zero");
为什么要用宏来查看?
因为 pid_status 中保存了好几条信息,只有前8位表示进程的退出状态,可以用宏来查看这8位的值。
运行 newshound2 程序,它会在退出前检查rssgossip.py脚本是否完成:
重定向输入、输出,然后让进程相互等待,进程间通信就这么简单。
问: 为了防止调用失败,调用exit()时需要检查它的返回值是否为-1吗?
答: 不需要,exit()不会失败,因此也就没有返回值。exit()是唯一没有返回值而且不会失败的函数。
问: 描述符表有多大?
答: 从0号到255号.
问: 我能用waitpid()等待其他进程吗?还是只有我启动的那些?
答: 你可以用waitpid()等待任何进程。
问 :为什么不能根据wait_pid(..., &pid_status, ...)中的pid_status直接判断退出状态?
答: 因为pid_status中还包含了其他信息。比如,如果一个进程自死亡 WIFSIGNALED(pid_status)就为假,若为他杀则为真.
问: pid_status明明是整型变量,怎么可以包含多条信息?
答: 用不同的位来保存不同的信息。pid_status的前8位保存了退出状态,而其他信息保存在了剩余那些位中。
问 : 是不是只要自行提取出pid_status的前8位,就可以不用WEXITSTATUS()?
答: 最好还是用WEXITSTATUS(),它不但可以提高代码的可读性,而且无论int在你的平台上有多大,程序都能正确工作。
问: 为什么WEXITSTATUS()要大写?
答: 因为WEXITSTATUS()是宏,不是函数。编译器运行时会把宏替换为一小段代码。
家书抵万金
你已经学会了用 exec() 和 fork() 运行独立进程,也知道怎么把子进程的输出重定向到文件,但如果你想从子进程直接获取数据呢?在进程运行时实时读取它生成的数据,而不
是等子进程把所有数据都发送到文件,再从文件中把数据读取出来,有这个可能吗?
为了举这个例子,rssgossip.py脚本提供了一个 -u 选项,可以用它显示找到新闻的URL:
linux命令:export RSS_FEED=http://syndication.eonline.com/syndication/feeds/rssfeeds/topstories.xml
linux命令:python rssgossip.py 'plan'
运行结果:
你完全可以运行脚本,然后把它的输出保存在文件中,不过这样很慢。如果父子进程能够在子进程运行期间通信,速度会快很多。
用管道连接进程
你曾用过某样东西实时连接两个进程,那就是管道。可以在命令行用管道把一个进程的输出连接到另一个进程的输入。在下面这个例子中,你手动运行了rssgossip.py脚本,然后把它的输出传给了 grep 命令, grep 找出了包含 http 的那些行:
当你在命令行用管道连接两条命令时,实际把它们当成了父子进程来连接,在上面的例子中, grep 命令是rssgossip.py脚本的父进程,过程如下:
1.命令行创建了父进程。
2.父进程在子进程中克隆出了rssgossip.py脚本。
3.父进程用管道把子进程的输出连接到自己的输入。
4.父进程运行了grep命令。
管道常用来在命令行中连接两个进程。下面看在C代码中如何连接两个进程.
案例研究:在浏览器中打开新闻
假设你想在浏览器中打开rssgossip.py脚本找到的新闻链接,你将在父进程中运行程序,在子进程中运行rssgossip.py。需要创建管道,把rssgossip.py的输出和程序的输入连接起来。
如何创建管道?---pipe() 打开两条数据流
每当打开数据流时,它都会加入描述符表。 pipe() 函数也是如此,它创建两条相连的数据流,并把它们加到表中,然后只要你往其中一条数据流中写数据,就可以从另一条数据流中读取。
pipe() 在描述符中创建这两项时,会把它们的文件描述符保存在一个包含两个元素的数组中:
int fd[2];
if (pipe(fd) == -1) { //把数组名传递给pipe()函数
error("Can't create the pipe");
}
pipe() 函数创建了管道,并返回了两个描述符:
fd[1]用来向管道写数据,
fd[0]用来从管道读数据。
你将在父、子进程中使用这两个描述符:
子进程
在子进程中,需要关闭管道的 fd[0] 端,然后修改子进程的标准输出,让它指向描述符 fd[1] 对应的数据流。
close(fd[0]);
dup2(fd[1], 1);
如此一来,子进程发送给标准输出的数据都会写到管道中。
父进程
在父进程中,需要关闭管道的 fd[1] 端(不需要写),然后重定向父进程的标准输入,让它从描述符 fd[0] 对应的数据流中读取数据:
dup2(fd[0], 0);
close(fd[1]);
如此一来,子进程写到管道的数据将由父进程的标准输入读取。
在浏览器中打开网页 示例代码:
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// 打印错误
void error(char *msg) {
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(1); // from -->stdlib.h //exit(1)会立刻终止程序,并把退出状态置1。
}
// 调用系统浏览器打开url
void open_url(char *url) {
char launch[255];
//sprintf(launch, "cmd /c start %s", url); // windows
//system(launch);
sprintf(launch, "x-www-browser '%s'", url); // linux
system(launch);
//sprintf(launch, "open '%s'", url); // mac
//system(launch);
}
int main(int argc, char *argv[]) {
char *phrase = argv[1];
char *vars[] =
{
"RSS_FEED=http://syndication.eonline.com/syndication/feeds/rssfeeds/topstories.xml",
// "RSS_FEED=http://www.msn.com/rss/MsnEntertainment.aspx",
NULL };
int fd[2];
if (pipe(fd) == -1) {
error("Can't create the pipe");
}
pid_t pid = fork();
if (pid == -1) {
error("Can't fork process");
}
printf("pid=%i\n", pid);
printf("!pid=%i\n", !pid);
if (!pid) {
// puts(phrase);
//把标准输出设为管道的写入端
dup2(fd[1], 1);
close(fd[0]);
if (execle("/usr/bin/python", "/usr/bin/python", "rssgossip.py", "-u",
phrase, NULL, vars) == -1) {
error("Can't run script");
}
}
dup2(fd[0], 0);
close(fd[1]);
char line[255];
while (fgets(line, 255, stdin)) {
if (line[0] == '\t')
open_url(line + 1);
}
return 0;
}
运行结果:
浏览器打开了:
管道是连接进程的好办法。现在你不但能够运行进程,控制它们的环境,而且还能获取进程的输出,这样就可以实现很多功能。任何一个能够在命令行中运行的程序你都可以在C代码中调用并控制它。
问: 管道是文件吗?
答: 这取决于操作系统创建管道的方式,通常用 pip e() 创建的管道都不是文件。
问: 就是说也有可能是文件?
答 : 你可以创建基于文件的管道,它们通常叫有名管道或FIFO(First In First Out,先进先出)文件。
问: 它能干嘛?
答: 因为基于文件的管道有名字,所以两个进程只要知道管道的名字也能用它来通信,即使它们非父子进程。
问: 太好了!怎么使用有名管道?
答: 使用mkfifo()系统调用,详情请见http://tinyurl.com/cdf6ve5。
问: 如果不用文件来实现管道,那用什么?
答:通常用存储器。数据写到存储器中的某个位置,然后再从另一个位置读取。
问: 如果我试图读取一个空的管道会怎么样?
答: 程序会等管道中出现东西。
问: 父进程如何知道子进程什么时候结束?
答: 子进程结束时,管道会关闭。fgets()将收到EOF(End Of File,文件结束符),于是fgets()函数返回0,循环就结束了。
问: 父进程能对子进程说话吗?
答: 当然可以。你完全可以反向连接管道,让数据从父进程发送到子进程。
问: 管道能够双向通信吗?这样父子进程不就可以边听边讲了?
答: 管道只能单向通信。不过可以创建两个管道,一个从父进程连到子进程,另一个从子进程连到父进程。
进程之死
比如你的程序正在从键盘读取数据,这时用户按了Ctrl+C,整个程序就停止运行了,这是为何?
操作系统用信号控制程序
当操作系统看到用户按了Ctrl+C,就会向程序发送中断信号。信号是一条短消息,即一个整型值。当信号到来时,进程必须停止手中一切工作去处理信号。进程会查看信号映射表,表中每个信号都对应一个信号处理器函数。中断信号的默认信号处理器会调用 exit() 函数。
操作系统为什么不直接结束程序,而是要在信号表中查找信号?
因为这样就可以在进程接收到信号时运行你自己的代码。
有时你希望在别人打断你的程序时运行自己的代码。假设进程打开了一些文件连接或网络连接,你希望在退出之前把它们关闭,并且做一些清理工作。当计算机在向你发送信号时,如何让它运行你的代码呢?可以用 sigaction 。
sigaction 是一个函数包装器
sigaction 是一个结构体,它有一个函数指针。 sigaction告诉操作系统进程收到某个信号时应该调用哪个函数。如果想在某人向进程发送中断信号时让操作系统调用 diediedie() 函数,就需要把 diediedie() 函数包装成 sigaction。
sigaction 的创建方法如下:
struct sigaction action; // 创建新动作
action.sa_handler = diediedie; // 想让计算机调用哪个函数
sigemptyset(&action.sa_mask); //用掩码来过滤 sigaction 要处理的信号,通常会用一空的掩码
action.sa_flags = 0; // 一些附加标志位,将他们置0即可.
sigaction 包装起来的那个函数就叫处理器,因为它将用来处理发送给它的信号,而处理器必须以特定的方式创建。
处理器必须接收信号参数
信号是一个整型值,如果你创建了一个自定义处理器函数,就需要接收一个整型参数,像这样:
void diediedie(int sig){ //sig是处理函数捕捉到的信号编号
puts ("Goodbye cruel world....\n");
exit(1);
}
因为我们以参数的形式传递信号,所以多个信号可以共用一个处理器;也可以为每个信号写一个处理器,完全由你做主。处理器的代码应该短而快,刚好能处理接收到的信号就好。
注意:在处理器函数中使用标准输出和标准错误时要小心。
虽然示例代码在标准输出中显示了文本,但在更复杂的程序中这么做时千万要小心。之所以会有信号就是因为程序中发生了故障,而故障可能就是标准输出无法使用,因此要小心。
用 sigaction() 来注册 sigaction
sigaction(signal_no, &new_action, &old_action);
1.signal_no,信号编号:这个整型值代表了你希望处理的信号。通常会传递 SIGINT或SIGQUIT这样的标准信号。
2.new_action,新动作:你想注册的新 sigaction 的地址.
3.old_action,旧动作:如果你想保存被替换的信号处理器, 可以再传一个sigaction 指针;如果不想保存,可以设置为NULL。
如果 sigaction() 函数失败,会返回-1,并设置 errno 变量。
代码示例:
#include <signal.h> // 需要包含 signal.h 头文件。
#include <stdio.h>
#include <stdlib.h>
// 捕捉到信号后的处理函数
void diediedie(int sig) {
puts("Goodbye cruel world....\n");
exit(1);
}
//注册处理器的函数
int catch_signal(int sig, void (*handler)(int)) {
// struct sighandler_t action;//windows中
struct sigaction action; // linux系统中的
action.sa_handler = handler;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
return sigaction(sig, &action, NULL);
// return signal(sig, &action);
}
int main() {
// SIGNIT--->表示中断信号
if (catch_signal(SIGINT, diediedie) == -1) {
fprintf(stderr, "Can't map the handler");
exit(2);
}
char name[30];
printf("Enter your name:");
fgets(name, 30, stdin);
printf("Hello %s\n", name);
return 0;
}
运行结果:
操作系统收到了Ctrl-C以后向进程发送 SIGINT 信号,然后进程运行了你的 diediedie() 函数。
操作系统向进程发送的各种信号,及对应引起它们的原因:
问: 如果中断信号的处理器不调用exit(),程序还能结束吗?
答: 不会。
问: 也就是说,我可以写一个忽略中断信号的程序?
答: 可以是可以,但这可不是什么好主意。程序收到错误信号以后最好还是退出,即使之前你运行了自己的代码。
用 kill 发送信号
怎么测试你写的信号处理代码呢?在类Unix操作系统中有一个叫 kill 的命令,之所以叫 kill 是因为这个命令通常用来“杀死”进程。事实上, kill 只是向进程发送了一个信号, kill 默认会向进程发送 SIGTERM 信号,你也可以用它发送其他信号。
我们打开两个终端试试。在一个终端运行程序,在另一个终端用 kill 向程序发送信号:
以上 kill 命令将向进程发送信号,然后运行进程中配置好的处理函数。但有一个例外,代码捕捉不到 SIGKILL 信号,也没法忽略它。也就是说,即使程序中有一个错误导致进程对任何信号都视而不见,还是能用 kill –KILL 结束进程.
也就是说:
命令kill -KILL <进程号> 一定可以送你的程序上西天!
另外,SIGSTOP 信号也是无法忽略的,它可以用来暂停进程.
用 raise() 发送信号
有时你想让进程向自己发送信号,这时就可以用 raise()函数。
raise(SIGTERM);
通常会在自定义的信号处理函数中使用 raise() ,这样程序就能在接收到低级别的信号时引发更高级别的信号。这叫
信号升级。
打电话叫程序起床
当计算机中发生了进程需要知道的事情时,操作系统就会向进程发送信号。比如用户想中断进程或“杀死”进程,或进程企图做一件它不应该做的事情,比如访问受限存储器。除了在发生错误时使用,有时进程也需要产生自己的信号,比如闹钟信号 SIGALRM 。闹钟信号通常由进程的间隔定时器创建。间隔定时器就像一台闹钟:你可以定一个时间,其间程序就会去做
其他事情:
alarm(120); //把闹钟调到120秒以后闹铃。
定时器发出 SIGALRM 信号
当进程收到信号以后就会停止手中一切工作来处理信号。进程在收到闹钟信号以后默认会结束进程,但通常情况下使用定时器不是为了让它帮你“杀死”程序,而是为了利用闹钟信号的处理器去做另一件事:
catch_signal(SIGALRM, pour_coffee);
alarm(120);
闹钟信号可以实现多任务。如果需要每隔几秒运行一个任务,或者想限制花费在某个任务上的时间,就可以用闹钟信号让程序打断自己。
注意:不要同时使用alarm()和sleep()。
sleep()函数会让程序沉睡一段时间 。和alarm() 函数一样,它也使用了间隔计时器,因此
同时使用这两个函数会发生冲突。
重置信号
你已经见过如何设置自定义信号处理器了,但如果你想还原默认的信号处理器怎么办?signal.h头文件中有一个特殊的符号 SIG_DFL ,它代表以默认方式处理信号。
catch_signal(SIGTERM, SIG_DFL);
忽略信号
你还可以用 SIG_IGN 符号让进程忽略某个信号。
catch_signal(SIGINT, SIG_IGN);
在你决定忽略某个信号前一定要慎重考虑,信号是控制进程和终止进程的重要方式,如果忽略了它们,程序就很难停下来。
问: 我能把闹钟定在几分之一秒后响铃吗?
答: 可以是可以,但很复杂。需要用另一个函数setitimer() ,它可以把进程间隔计时器的单位设为几分之一秒。详情请见http://tinyurl.com/3o7hzbm。
问: 为什么一个进程只有一个定时器?
答: 定时器由操作系统的内核管理,如果一个进程有很多定时器,内核就会变得很慢,因此操作系统需要限制进程能使用的定时器个数。
问: 定时器可以实现多任务?太好了,也就是说我能同时做几件事?
答: 非也。别忘了,进程在处理信号时会停止一切工作,也就是说一次只能做一件事,稍后会看到如何让代码同时做多件事。
问: 重复设置定时器会怎么样?
答: 每次调用alarm()函数都会重置定时器,也就是说如果把闹钟调到10秒,但过一会儿又把它设为了10分钟,那么闹钟信号10分钟以后才会触发,第一个10秒计时就失效了。
练习:
这个程序用来测试用户的数学水平,它要求用户做乘法。程序的结束条件如下:
1. 用户按了Ctrl-C。
2. 回答时间超过5秒。
程序在结束时会显示最终得分并把退出状态设为0。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
int score = 0; //记录总分
//退出程序的处理函数
void end_game(int sig) {
printf("\nFinal score: %i\n", score);
exit(0);
}
//注册信号处理器函数
int catch_signal(int sig, void (*handler)(int)) {
struct sigaction action;
action.sa_handler = handler;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
return sigaction(sig, &action, NULL);
}
// 时间到了的处理函数
void times_up(int sig) {
puts("\nTIME'S UP!");
//引发SIGINT信号,让程序调用 end_game()显示最后得分。
raise(SIGINT);
}
void error(char* msg) {
fprintf(stderr, "%s: %s\n", msg, strerror(errno));
exit(1);
}
int main() {
catch_signal(SIGALRM, times_up);
catch_signal(SIGINT, end_game);
srandom(time(0)); //确保每次都得到不同的随机数
while (1) {
int a = random() % 11; //a是0~10的随机数
int b = random() % 11;
char txt[4];
// 将闹钟信号设为5秒后触发。
// 只要能在5秒内再次回到到这个地方,定时器就会重置,闹钟信号也不会触发。
alarm(5);
printf("\nWhat is %i times %i? ", a, b);
fgets(txt, 4, stdin);
int answer = atoi(txt);
if (answer == a * b)
score++;
else
printf("\nWrong! Score: %i\n", score);
}
return 0;
}
运行结果:
第一次没有回答等了5秒,程序自动结束,第二次按了ctr+c,程序也结束了,都算了总分.
虽然信号有些复杂,但很好用。信号可以让程序从容结束,而间隔定时器可以帮助处理一些超时任务。
问: 信号按什么顺序发送,程序就会按什么顺序接收吗?
答: 如果两个信号发送间隔很短就不会,操作系统会先发送它认为更重要的信号。
问: 总是如此吗?
答: 取决于你的平台。例如在Cygwin的很多版本中,信号会按发送的顺序接收,但通常不应该做这样的假设。
问: 如果一个信号发送了两次, 进程都会接收到吗?
答: 还是要看情况,在Linux和Mac中,如果一个信号在很短的时间里发送了两次,内核只会发送其中的一个;而在Cygwin中两个信号都会发送,但不应该做这样的假设。