如果进程是SIGKILLed,操作系统(POSIX)是否完成对内存映射文件的修改?

时间:2018-07-19 22:43:34

标签: c linux unix posix

类似的post讨论了在SIGKILL之后是否将对内存映射文件的更改刷新到磁盘,但是如果在执行更改的过程中对SIGKILL进行了处理(例如在将其刷新到磁盘之前将其写入/删除到内存缓冲区?

基础文件是否已更新和损坏?在终止进程之前,写/删除操作是否已完成?有什么保障措施吗?

1 个答案:

答案 0 :(得分:3)

假设您有类似的东西

volatile unsigned char  *map; /* memory-mapped file */
size_t                   i;

for (i = 0; i < 1000; i++)
    map[i] = slow_calculation(i);

出于某种原因,该过程在i = 502时被终止。

在这种情况下,文件的内容确实会反映该点的映射内容。

不,没有办法避免这种情况(关于KILL信号),因为KILL是不可阻塞且不可捕获的。

通过使用临时缓冲区作为“事务”缓冲区,将新值计算到该缓冲区,然后将值复制过来,可以最小化窗口。这不能保证,但这确实意味着即使进程被杀死,文件内容完整的可能性也更高。 (此外,这意味着如果您使用互斥锁来同步对映射的访问,则只需将互斥锁保持最短的时间即可。)

通过KILL信号杀死进程是非常异常的终止,并且我认为这是导致内存映射文件出现乱码的原因。完全不应该在正常操作期间执行此操作;则使用TERM信号。

应该担心的是,您的过程会及时响应TERM信号。 TERM是可捕获且可阻塞的,并且基本上是外部主管进程(或该进程所属的用户或超级用户)尽快要求该进程完全退出的一种方式。但是,该过程不应该重蹈覆辙,因为如果在收到TERM信号后几秒钟内没有退出该过程,则发送该过程的KILL信号是很常见的。

在我自己的守护程序中,除非系统负载很重,否则我会努力让它们在几秒钟内响应TERM。当然,这是一个非常主观的测量,因为不同系统的速度各不相同,但是这里没有硬性规定。

处理此问题的一种方法是安装一个TERM信号处理程序,该信号处理程序在正常操作中会立即终止该过程。对于关键部分,退出被推迟:

static volatile int  in_critical = 0;
static volatile int  need_to_exit = 0;

static void handle_exit_signal(int signum)
{
    __atomic_store_n(&need_to_exit, 1, __ATOMIC_SEQ_CST);
    if (!__atomic_load_n(&in_critical, __ATOMIC_SEQ_CST))
        exit(126);
}

static int install_exit(int signum)
{
    struct sigaction  act;
    memset(&act, 0, sizeof act);
    sigemptyset(&act.sa_mask);
    act.sa_handler = handle_exit_signal;
    act.sa_flags = SA_RESTART;
    if (sigaction(signum, &act, NULL) == -1)
        return errno;
    return 0;
}

要进入和退出关键部分(例如,当您在共享内存区域中持有互斥锁时):

static inline void critical_begin(void)
{
    __atomic_add_fetch(&in_critical, 1, __ATOMIC_SEQ_CST);
}

static inline void critical_end(void)
{
    if (!__atomic_sub_fetch(&in_critical, 1, __ATOMIC_SEQ_CST))
        if (__atomic_load_n(&need_to_exit, __ATOMIC_SEQ_CST))
            exit(126);
}

因此,如果您在关键区域中时收到了TERM信号(并且critical_begin()critical_end()嵌套),则对critical_end()的最终调用将退出该过程。

请注意,即使信号处理程序在其他线程中执行,我也使用GCC atomic built-ins来原子地管理标志,而没有数据争用。我发现这是的最干净的解决方案,尽管它也可以在其他操作系统上使用。 (您可以在Linux中使用的其他C编译器(例如clang和Intel CC)也支持它们。)

因此,在伪代码中,如开头所示进行1000个元素的缓慢计算将是

volatile unsigned char  *map;
unsigned char            cache[1000];
size_t                   i;

/* Nothing critical yet, we're just calculating new values... */
for (i = 0; i < 1000; i++)
    cache[i] = slow_calculation(i);

/* Update shared memory map. */
critical_begin();
/* pthread_mutex_lock() */
memcpy(map, cache, 1000);
/* pthread_mutex_unlock() */
critical_end();

如果在critical_begin()之前传送了TERM信号,则该过程将在那里终止。如果在此之后但在critical_end()之前发送了TERM信号,则对critical_end()的调用将终止该过程。

这只是可以解决潜在问题的一种模式;还有其他。信号处理程序设置为具有单个volatile sig_atomic_t done = 0;且信号处理程序设置为非零且主要处理循环定期检查的代码更为常见。

如R ..在评论中指出的那样,用于引用内存映射的指针应该是指向volatile(即volatile some_type *map)的指针,以阻止编译器将存储重新排序到内存映射