显然课程中的意图是将代码修改为
-
使用单独的线程,在循环中接收调用sigwait() 或sigwaitinfo() 的信号。信号必须被阻塞(首先,并且一直,对于所有线程),或者操作未指定(或者信号被传递到另一个线程)。
这种方式本身没有信号处理函数,这将仅限于异步信号安全函数。调用sigwait()/sigwaitinfo() 的线程是完全正常的线程,不受任何与信号或信号处理程序相关的限制。
(还有其他接收信号的方法,例如使用设置全局标志的信号处理程序,并进行循环检查。大多数会导致 busy-waiting,运行 do -nothing 循环,无用地消耗 CPU 时间:一个非常糟糕的解决方案。我在这里描述的方式不会浪费任何 CPU 时间:内核将在调用 sigwait()/sigwaitinfo() 时将线程置于睡眠状态,并且仅在信号到达。如果要限制睡眠时长,可以使用sigtimedwait()代替。)
-
自从printf() 等。不保证是线程安全的,您可能应该使用pthread_mutex_t 来保护输出到标准输出——换句话说,这样两个线程就不会尝试同时输出。
在 Linux 中这不是必需的,因为 GNU C printf()(_unlocked() 版本除外)是线程安全的;对这些函数的每次调用都已经使用了一个内部互斥体。
注意C库可能会缓存输出,所以要确保输出数据,需要调用fflush(stdout);。
如果您想以原子方式使用多个printf()、fputs() 或类似调用,而其他线程无法在其间注入输出,则互斥锁特别有用。因此,建议使用互斥锁,即使在简单情况下在 Linux 上不需要它。 (是的,您确实想在持有互斥锁时也执行fflush(),尽管如果输出阻塞可能会导致互斥锁被持有很长时间。)
我个人会以完全不同的方式解决整个问题——我会在信号处理程序中使用write(STDERR_FILENO,) 输出到标准错误,并将主程序输出到标准输出;没有线程或任何特殊需要,只是信号处理程序中的一个简单的低级写循环。严格来说,我的程序的行为会有所不同,但对于最终用户来说,结果看起来非常相似。 (除了可以将输出重定向到不同的终端窗口,并并排查看它们;或将它们重定向到辅助脚本/程序,这些脚本/程序将纳秒挂钟时间戳添加到每个输入行;以及在调查时有用的其他类似技巧东西。)
就个人而言,我发现了从原始问题到“正确解决方案”的跳跃——如果我所描述的确实是正确的解决方案;我确实认为这有点牵强。当 Saf 提到正确的解决方案应该使用 pthreads 时,我才意识到这种方法。
我希望您能从中找到信息,但不要剧透。
2013 年 3 月 13 日编辑:
这是我用来安全地将数据从信号处理程序写入描述符的writefd() 函数。我还包括了包装函数wrout() 和wrerr(),您可以使用它们分别将字符串写入标准输出或标准错误。
#include <unistd.h>
#include <string.h>
#include <errno.h>
/**
* writefd() - A variant of write(2)
*
* This function returns 0 if the write was successful, and the nonzero
* errno code otherwise, with errno itself kept unchanged.
* This function is safe to use in a signal handler;
* it is async-signal-safe, and keeps errno unchanged.
*
* Interrupts due to signal delivery are ignored.
* This function does work with non-blocking sockets,
* but it does a very inefficient busy-wait loop to do so.
*/
int writefd(const int descriptor, const void *const data, const size_t size)
{
const char *head = (const char *)data;
const char *const tail = (const char *)data + size;
ssize_t bytes;
int saved_errno, retval;
/* File descriptor -1 is always invalid. */
if (descriptor == -1)
return EINVAL;
/* If there is nothing to write, return immediately. */
if (size == 0)
return 0;
/* Save errno, so that it can be restored later on.
* errno is a thread-local variable, meaning its value is
* local to each thread, and is accessible only from the same thread.
* If this function is called in an interrupt handler, this stores
* the value of errno for the thread that was interrupted by the
* signal delivery. If we restore the value before returning from
* this function, all changes this function may do to errno
* will be undetectable outside this function, due to thread-locality.
*/
saved_errno = errno;
while (head < tail) {
bytes = write(descriptor, head, (size_t)(tail - head));
if (bytes > (ssize_t)0) {
head += bytes;
} else
if (bytes != (ssize_t)-1) {
errno = saved_errno;
return EIO;
} else
if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) {
/* EINTR, EAGAIN and EWOULDBLOCK cause the write to be
* immediately retried. Everything else is an error. */
retval = errno;
errno = saved_errno;
return retval;
}
}
errno = saved_errno;
return 0;
}
/**
* wrout() - An async-signal-safe alternative to fputs(string, stdout)
*
* This function will write the specified string to standard output,
* and return 0 if successful, or a nonzero errno error code otherwise.
* errno itself is kept unchanged.
*
* You should not mix output to stdout and this function,
* unless stdout is set to unbuffered.
*
* Unless standard output is a pipe and the string is at most PIPE_BUF
* bytes long (PIPE_BUF >= 512), the write is not atomic.
* This means that if you use this function in a signal handler,
* or in multiple threads, the writes may be interspersed with each other.
*/
int wrout(const char *const string)
{
if (string)
return writefd(STDOUT_FILENO, string, strlen(string));
else
return 0;
}
/**
* wrerr() - An async-signal-safe alternative to fputs(string, stderr)
*
* This function will write the specified string to standard error,
* and return 0 if successful, or a nonzero errno error code otherwise.
* errno itself is kept unchanged.
*
* You should not mix output to stderr and this function,
* unless stderr is set to unbuffered.
*
* Unless standard error is a pipe and the string is at most PIPE_BUF
* bytes long (PIPE_BUF >= 512), the write is not atomic.
* This means that if you use this function in a signal handler,
* or in multiple threads, the writes may be interspersed with each other.
*/
int wrerr(const char *const string)
{
if (string)
return writefd(STDERR_FILENO, string, strlen(string));
else
return 0;
}
如果文件描述符引用管道,writefd() 可用于以原子方式写入最多PIPE_BUF(至少 512)字节。 writefd() 也可用于 I/O 密集型应用程序,以将信号(如果使用 sigqueue() 引发,则相关值、整数或指针)转换为套接字或管道输出(数据),使其更容易多路复用多个 I/O 流和信号处理。变体(带有标记为 close-on-exec 的额外文件描述符)通常用于轻松检测子进程是执行了另一个进程还是失败了;否则很难检测出哪个进程——原来的子进程,还是执行的进程——退出了。
在此答案的 cmets 中,有一些关于 errno 的讨论,以及 write(2) 修改 errno 是否使其不适合信号处理程序这一事实感到困惑。
首先,POSIX.1-2008(及更早版本)将async-signal-safe 函数定义为可以从信号处理程序安全调用的函数。 2.4.3 Signal actions 章节包括此类函数的列表,包括 write()。请注意,它还明确指出 “获取 errno 值的操作和为 errno 分配值的操作应是异步信号安全的。”
这意味着 POSIX.1 打算将 write() 安全地用于信号处理程序中,并且还可以对 errno 进行操作以避免被中断的线程看到 errno 中的意外变化。
因为errno是线程局部变量,所以每个线程都有自己的errno。传递信号时,它总是会中断进程中现有的线程之一。信号可以定向到特定线程,但通常内核决定哪个线程获得进程范围的信号;它因系统而异。如果只有一个线程,即初始线程或主线程,那么显然是被中断的线程。所有这一切意味着,如果信号处理程序保存它最初看到的errno 的值,并在它返回之前恢复它,那么对errno 的更改在信号处理程序之外是不可见的。
有一种方法可以检测它,但是,在 POSIX.1-2008 中也通过谨慎的措辞暗示:
从技术上讲,&errno 几乎总是有效的(取决于所应用的系统、编译器和标准),并产生包含当前线程错误代码的 int 变量的地址。因此,另一个线程可以监视另一个线程的错误代码,是的,这个线程会在信号处理程序中看到对它的更改。但是,不能保证其他线程能够原子地访问错误代码(尽管它在许多架构上是原子的):这种“监视”无论如何都只会提供信息。
遗憾的是,几乎所有 C 中的信号处理程序示例都使用 stdio.h printf() 等等。不仅在许多层面上都是错误的——从非异步安全到缓存问题,可能是对FILE 字段的非原子访问,如果被中断的代码同时也在执行 I/O——,而且使用unistd.h 的正确 解决方案与我在此编辑中的示例类似,同样简单。在信号处理程序中使用 stdio.h I/O 的基本原理似乎是“它通常有效”。我个人讨厌这一点,因为例如暴力也“通常有效”。我认为它愚蠢和/或懒惰。
我希望你发现了这些信息。