微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

C信号处理程序中的竞态条件

我正在做一些课程,我们已经拿到了下面的代码。 有些问题会问各种代码行,这很好,而且我明白,但曲线球是“这个程序包含一个竞争条件,在哪里和为什么会出现?

代码

#include <stdio.h> #include <signal.h> static void handler(int signo) { printf("This is the SIGUSR1 signal handler!n"); } int main(void) { sigset_t set; sigemptyset(&set); sigset(SIGUSR1,handler); sigprocmask(SIG_SETMASK,&set,NULL); while(1) { printf("This is main()!n"); } return 0;

}

我在想,竞赛条件是,当信号到达时,无法知道“这是主”还是“这是SIGUSR1”的顺序,但是如果有人能够确认或澄清这一点非常感激。 他还询问如何解决这个问题(比赛条件),而不是寻找一个完整的答案,但任何提示将不胜感激。

sigaction结构和函数

日志旋转Nginx在Windows上

如何从内核模块发送“信号”到用户空间应用程序?

在C中重新启动程序

O_ASYNC停止产生SIGIO

我怎么知道SIGILL是来自非法指令还是kill -ILL?

返回无法处理的POSIX信号的代码

解释void(* signal(int signo,void *(func)(int)))(int)

出现信号处理程序时出现分段错误

当它杀死一个进程时,Linux释放spinlock /信号量?

显然这个过程的意图是修改代码

使用一个单独的线程,在一个循环中接收调用sigwait()或sigwaitinfo()的信号。 信号必须被阻塞(对于所有的线程,首先和一直),或者操作没有被指定(或者信号被传送到另一个线程)。

这样就没有信号处理函数本身,这将被限制为异步信号安全函数调用sigwait() / sigwaitinfo()线程是一个完全正常的线程,不受任何与信号或信号处理程序有关的限制。

(还有其他方法可以接收信号,例如使用设置全局标志的信号处理程序,以及循环检查的信号处理程序,其中大多数会导致忙等待 ,运行一个无所事事的循环,无用地烧毁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-03-13:

这里是writefd()函数我用来安全地将数据从信号处理程序写入描述符。 我还包括了可以用来将字符串写入标准输出或标准错误的wrapper函数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,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 writes may be interspersed with each other. */ int wrerr(const char *const string) { if (string) return writefd(STDERR_FILENO,strlen(string)); else return 0; }

如果文件描述符指向一个管道, writefd()可以用来自动写入PIPE_BUF (至少512)个字节。 writefd()也可以用在I / O密集型应用程序中,用于将信号(如果使用sigqueue() ,相关值,整数或指针)引发到套接字或管道输出(数据),使得它更容易复用多个I / O流和信号处理。 一个变体(带有额外的文件描述符标记为执行关闭)通常用于轻松检测子进程是执行另一个进程还是失败; 否则很难检测到哪个进程 – 原来的子进程或执行进程 – 退出

在对这个答案的评论中,有一些关于errno讨论,以及write(2)修改errno的事实是否使得它不适合信号处理程序。

首先,POSIX.1-2008(及更早版本)将异步信号安全函数定义为可以从信号处理程序安全地调用的那些函数。 2.4.3信号动作章节包括这些函数的列表,包括write() 。 请注意,它也明确规定: “获取errno值的操作和将值赋给errno的操作应该是异步信号安全的。

这意味着POSIX.1意图在信号处理程序中使用write()函数是安全的,并且还可以对errno进行操作,以避免中断的线程在errno看到意外的更改。

由于errno是一个线程局部变量,每个线程都有自己的errno 。 当一个信号被传递时,它总是中断在这个过程中的一个现有的线程。 信号可以定向到一个特定的线程,但通常内核决定哪个线程得到一个全过程的信号; 它在不同系统之间变化 如果只有一个线程,初始线程或主线程,那显然是被中断的线程。 所有这一切意味着如果信号处理程序保存了它最初看到的errno的值,并且在它返回之前恢复它,那么对errno的更改在信号处理程序之外是不可见的。

有一种方法可以检测到 ,但是在POSIX.1-2008中也用了谨慎的措辞来暗示:

从技术上讲, &errno几乎总是有效的(取决于所应用的系统,编译器和标准),并产生保存当前线程的错误代码的int变量的地址。 因此,另一个线程可能会监视另一个线程的错误代码,是的,这个线程会看到在信号处理程序中完成的更改。 但是,并不能保证其他线程能够以原子方式访问错误代码(尽管在许多体系结构中它是原子的):这样的“监视”只能是信息性的。

可惜的是,C中几乎所有的信号处理器例子都使用了stdio.h printf()等等。 不仅在许多层面上是错误的 – 从非异步安全到缓存问题,如果被中断的代码同时也在执行I / O操作,可能会对FILE字段进行非原子访问 – 但是正确的在这个编辑中使用unistd.h类似于我的例子的解决方案就是一样简单。 在信号处理程序中使用stdio.h I / O的基本原理似乎是“它通常起作用”。 我个人感到厌恶,因为例如暴力也是“通常有效”的。 我认为这是愚蠢的和/或懒惰的。

我希望你发现这个信息。

没有比赛条件, 比这更糟糕。 根据POSIX标准, 程序的行为是不确定的 (如果信号在正确的时刻传递)。

查看man 7信号手册页,具体来说是异步信号安全功能下的部分:

信号处理函数必须非常小心,因为在其他地方的处理可能会在程序执行的某个任意点被中断。 POSIX有“安全功能”的概念。 如果一个信号中断了一个不安全的函数的执行,并且处理程序调用一个不安全的函数,那么程序的行为是不确定的。

请注意, printf()绝对不是一个异步信号安全函数 ; 因此行为是不确定的。

在一般情况下,解决方案是非平凡的,因为没有异步信号安全锁定原语(除了sem_post() ,本身不足以解决这个问题,还有文件锁定,必须在所有的printf()电话)。 通用的便携式解决方案是使用unistd.h pipe()创建管道,并使用write()将输出write()管道,并从管道中读取主程序并“转发”内容。 比PIPE_BUF短的POSIX保证是原子性的, PIPE_BUF至少为512(在Linux中为4096) – 有关详细信息,请参阅man 7 pipe ,因此在实践中,对于便携式代码,这也限制为512字节或更短的消息。

通常情况下,在这种特殊情况下,用信号处理程序中的printf()替换全局volatile sigatomic_t变量就足够了。 主循环可以简单地检查(并清除)全局变量输出消息本身。

虽然标志变量方法可能会丢失快速重复的SIGUSR1信号,但是这是无关紧要的,因为总是可以丢失快速重复的SIGUSR1信号:一次只能有一个信号处于待处理状态,因此在第一个和处理之间发生的重复信号不是交付! (如果要使用像SIGRTMIN+0这样排队的实时信号,可以通过使用原子内置__sync_fetch_and_and(variable,0)如__sync_fetch_and_and(variable,0)或__atomic_exchange_n(variable,__ATOMIC_SEQ_CST)循环和__sync_fetch_and_add(variable,1)或__atomic_fetch_add(variable,1,__ATOMIC_SEQ_CST)在信号处理程序中;前面都有一个__sync_synchronize()或__atomic_signal_fence(__ATOMIC_SEQ_CST)调用,以确保更改立即对另一个有效/可见。但是在这种情况下你不需要担心原子操作。)

关于sigset()和sigprocmask() ,还有一个有趣的角落案例 – 不是竞争条件。 一个进程从它的父节点继承它的信号掩码,而SIGUSR1在认情况下不被阻塞。 除非处理,否则会导致进程终止。 因此,根据继承的信号掩码,在sigset()调用之前传递的SIGUSR1信号被阻塞,或导致进程终止。 (但是,如果set包含SIGUSR1 ,即SIGUSR1被阻塞,那么将会有竞争条件,除非在sigset()之前sigset() sigprocmask() ,但由于set是空的,所以sigset()最好在sigprocmask()之前调用。 )

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐