Linux下中断程序导致写文件失败的分析

案例:

一个普通linux C程序,执行期间会进行多次printf操作,利用bash脚本重定向功能,将stdout重定向到一个另一个文件中去。在运行途中用ctrl+C终止程序,发现定向文件始终为空,即写失败。

分析:

原本以为是bash重定向机制导致的问题,于是将重定向取消,改为使用fprintf,而非printf。即在C程序内部进行写文件。发现问题依旧。(排除fopen打开失败的因素)

仔细观察,发现问题集中在两个层面,一个是ctrl+c到底做了什么,二是写文件操作为什么失败。

首先,ctrl+C不代表杀死进程,仅仅代表中断。中断和杀死并不是完全一样的概念。google了一下,有以下发现:

“Linux中的kill命令用来终止指定的进程(terminate a process)的运行,是Linux下进程管理的常用命令。通常,终止一个前台进程可以使用Ctrl+C键,但是,对于一个后台进程就须用kill命令来终止,我们就需要先使用ps/pidof/pstree/top等工具获取进程PID,然后使用kill命令来杀掉该进程。kill命令是通过向进程发送指定的信号来结束相应进程的。在默认情况下,采用编号为15的TERM信号。TERM信号将终止所有不能捕获该信号的进程。对于那些可以捕获该TERM信号的进程就要用编号为9的kill信号,强行“杀掉”该进程。” 

 “

1、kill命令可以带信号号码选项,也可以不带。如果没有信号号码,kill命令就会发出终止信号(15),这个信号可以被进程捕获,使得进程在退出之前可以清理并释放资源。也可以用kill向进程发送特定的信号。例如:

kill -2 123

它的效果等同于在前台运行PID为123的进程时按下Ctrl+C键。但是,普通用户只能使用不带signal参数的kill命令或最多使用-9信号。

2、kill可以带有进程ID号作为参数。当用kill向这些进程发送信号时,必须是这些进程的主人。如果试图撤销一个没有权限撤销的进程或撤销一个不存在的进程,就会得到一个错误信息。

3、可以向多个进程发信号或终止它们。

4、当kill成功地发送了信号后,shell会在屏幕上显示出进程的终止信息。有时这个信息不会马上显示,只有当按下Enter键使shell的命令提示符再次出现时,才会显示出来。

5、应注意,信号使进程强行终止,这常会带来一些副作用,如数据丢失或者终端无法恢复到正常状态。发送信号时必须小心,只有在万不得已时,才用kill信号(9),因为进程不能首先捕获它。要撤销所有的后台作业,可以输入kill 0。因为有些在后台运行的命令会启动多个进程,跟踪并找到所有要杀掉的进程的PID是件很麻烦的事。这时,使用kill 0来终止所有由当前shell启动的进程,是个有效的方法。

6、只有第9种信号(SIGKILL)才可以无条件终止进程,其他信号进程都有权利忽略。 下面是常用的信号:

HUP    1    终端断线

INT     2    中断(同 Ctrl + C)

QUIT    3    退出(同 Ctrl + \)

TERM   15    终止

KILL    9    强制终止

CONT   18    继续(与STOP相反, fg/bg命令)

STOP    19    暂停(同 Ctrl + Z)

至此,可以得出一个结论:我们平常所按ctrl+C不等价于“终止进程”。ctrl+C一般情况下等价于kill -s SIGINT。即进程接受的是SIGINT信号。而接受了SIGINT信号并不是简单的杀死进程。

在一般程序编写中,可以利用signal.h中所包含的相关函数(如sigaction)来对这些不同类别的信号进行响应,从而进行对应的后处理,如资源的释放等等。

如此,在bash脚本中如若出现:

$cmd &

spid=$!

kill -s SIGINT $spid

是无法杀死进程的。进程仍旧在后台运行。若改成kill -9 $spid。便可杀死进程。

扯的有点远了。回到之前的问题,无论采取上述哪种kill方式,都会导致写文件失败。

那面对这些kill,平常的程序到底如何应对写文件之类的操作呢?再google一下,得到以下有用信息:

“SIGINT can be caught and handled. It's like telling the process "Please stop what you're doing." The process is free to ignore the signal, or implement a handler that does anything it wants. The default behavior is to terminate, and this is what most processes will do. It's typical, but not required, for a process to handle SIGINT by gracefully terminating -- closing any open files, network connections, or database handles and stopping the current operation in such a way that prevents data loss or corruption.

SIGKILL can't be handled by the receiving process. If a process gets sent SIGKILL, it's toast, period. If a process receives SIGKILL in the middle of a database transaction or file write (for instance), it has no chance to exit gracefully and data loss or corruption may occur.

When you do Ctrl-C in the terminal, the terminal sends SIGINT to the running process. Like I said before, most processes will gracefully terminate on receiving SIGINT. Once it's terminated, it's just as surely ended as if it had been SIGKILL'ed or exited normally -- you shouldn't expect to see it in ps because it's still gone.

But there are some programs that don't respond to SIGINT in that way. bash is one example; if you hit Ctrl-C at a shell prompt, it'll just cancel whatever you've typed on that line -- not terminate the whole shell. Again, this is because the behavior when a process receives SIGINT is determined by the process itself. vi is another program that doesn't handle SIGINT by terminating.”

和我们上面分析的一样,大部分的程序都需要一个handler来应对SIGINT信号。文中提到了exit normally。看来只有正常退出才能保证写文件成功?

stackoverflow上面有一个问答正好解决了我的疑问。

Q:

I've read in a man page that when exit() is called all streams are flushed and closed automatically. At first I was skeptical as to how this was done and whether it is truly reliable but seeing as I can't find out any more I'm going to accept that it just works — we'll see if anything blows up. Anyway, if this stream closing behavior is present in exit() is such behavior also present in the default handler for SIGINT (the interrupt signal usually triggered with Ctrl+C)? Or, would it be necessary to do something like this:

#include <signal.h>
#include <stdlib.h>

void onInterrupt(int dummy) { exit(0); }

int main() {
   signal(SIGINT, onInterrupt);
   FILE *file = fopen("file", "a");
   for (;;) { fprintf(file, "bleh"); } }

to get file to be closed properly? Or can the signal(SIG... and void onInterrupt(... lines be safely omitted?

Please restrict any replies to C, C99, and POSIX as I'm not using GNU libc. Thanks.

A:

1.So in C99, if it's closed then it's flushed.

2.You'll have to handle the signal if you want your buffers flushed. Otherwise the process will be terminated and the file descriptors closed without flushing the stdio buffers.

By default, a SIGINT will terminate the process abnornally. Processes so terminated do not call exit() and so do not have buffers flushed.

也就是说,只有正常退出,才能做到flush。否则将写失败。

之后有百度了下中文资料,发现同样的结论。

“fflush库函数的作用是把文件流里的所有未写出数据立刻写出。例如,你可以用这个函数来确保在试图读入一个用户响应之前,先向终端送出一个交互提示符。使用这个函数还可以确保在程序继续执行之前重要的数据都已经被写到磁盘上。有时在调试程序时,还可以用它来确定程序是正在写数据而不是被挂起了。注意,调用fclose函数隐含执行了一次flush操作,所以不必在fclose之前调用fflush。

fclose库函数关闭指定的文件流stream,使所有尚未写出的数据都写出。因为stdio库会对数据进行缓冲,所以使用fclose是很重要的。如果程序需要确保数据已经全部写出,就应该调用fclose函数。虽然当程序正常结束时,会自动对所有还打开的文件流调用fclose函数,但这样做就没有机会检查由fclose报告的错误了。与文件描述符一样,可用文件流的数目也是有限制的。这个限制由头文件stdio.h中的FOPEN_MAX常量定义,最小为8。

“所谓flush一个缓冲,是指对写缓冲而言,将缓冲内的数据全部写入实际的文件,并将缓冲清空,这样可以保证文件处于最新的状态。之所以需要flush,是因为写缓冲使得文件处于一种不同步的状态,逻辑上一些数据已经写入了文件,但实际上这些数据仍然在缓冲中,如果此时程序意外地退出(发生异常或断电等),那么缓冲里的数据将没有机会写入文件。flush可以在一定程度上避免这样的情况发生。”

所以说,平时咱写程序,需要谨慎和按流程来,fclose做的事情有很多,不要全指望main函数return后自动帮你close文件。因为一旦出现上述中断的情形,就会生问题。

 

解决方案:

分析了这么多,解决方案相比也很明了了。

方案一:

在C程序中加入SIGINT响应函数,保证程序正常退出。

方案二:

在C程序中加入fflush函数,保证所有输出第一时间写入文件。

 

方案一才是最好的解决方案,而方案二有些hack了。

就这样。

posted @ 2015-04-18 21:21  IT屁民  阅读(2756)  评论(0编辑  收藏  举报