用户和内核空间之间的共享信号量

Posted

技术标签:

【中文标题】用户和内核空间之间的共享信号量【英文标题】:Shared semaphore between user and kernel spaces 【发布时间】:2013-06-30 14:19:00 【问题描述】:

短版

是否可以在用户空间和内核空间之间共享信号量(或任何其他同步锁)? Named POSIX semaphores have kernel persistence,这就是为什么我想知道是否可以从内核上下文中创建和/或访问它们。

由于关于正常使用 POSIX 信号量的信息海量,搜索互联网并没有太大帮助。

加长版

我正在开发一个unified interface to real-time systems,其中我有一些额外的簿记需要处理,受信号量保护。这些簿记是在资源分配和释放上完成的,这是在非实时环境中完成的。

使用 RTAI,等待和发布信号量的线程需要处于实时上下文中。这意味着使用 RTAI 的命名信号量意味着在用户空间中的每个等待/发布时在实时和非实时上下文之间切换,更糟糕的是,为内核空间中的每个 sem/等待创建一个短的实时线程。

我正在寻找一种在内核空间和用户空间之间共享普通 Linux 或 POSIX 信号量的方法,以便我可以在非实时上下文中安全地等待/发布它。

任何有关此主题的信息将不胜感激。如果这不可能,您还有其他想法如何完成这项任务吗?1

1 一种方法是添加系统调用,在内核空间中使用信号量,并让用户空间进程调用该系统调用,并且信号量将全部在内核空间中管理.如果我不必仅仅因为这个而修补内核,我会更高兴。

【问题讨论】:

我想会有一些涉及用户端上下文切换的复杂情况......不过只是猜测。 @DrewMcGowen,我猜因为内核知道信号量,内核中应该有 do_sem_wait 类型的函数(内核是否仍然有 do_X 用于内核方面X?) 或者可以解决这些问题的东西。从用户的角度来看,一切都是正常的 POSIX 信号量。事实上,我只是想从内核访问相同的 POSIX 信号量。 内核在哪里?您是否希望与 调度程序 共享用户空间信号量?你看到问题了吗?您必须更具体地了解内核空间。您不能在中断处理程序中使用up()down(),而不能单独使用一些页面错误和其他毛茸茸的地方,即内核空间。 @artlessnoise,明确一点,我对中断处理程序不感兴趣。 我认为只有内核线程才有意义?许多子系统也通过故障处理程序调用。 【参考方案1】:

嗯,你的方向是对的,但并不完全——

Linux 命名的 POSIX 信号量基于 FUTex,它代表快速用户空间互斥锁。顾名思义,虽然它们的实现是由内核辅助的,但其中很大一部分是由用户代码完成的。在内核和用户空间之间共享这样一个信号量需要在内核中重新实现这个基础设施。可能,但肯定不容易。

另一方面,SysV 信号量完全在内核中实现,用户空间只能通过标准系统调用(例如sem_timedwait() 和朋友)访问。

这意味着每个与 SysV 相关的操作(信号量创建、获取或释放)实际上都在内核中实现,您只需从代码中调用底层内核函数即可从内核中获取相同的信号量。

因此,您的用户代码将简单地调用sem_timedwait()。这是最简单的部分。

内核部分有点棘手:您必须在内核中找到实现sem_timedwait() 和相关调用的代码(它们都在文件 ipc/sem.c 中)并创建一个副本无需调用copy_from_user(...)copy_to_user(..) 和朋友即可执行原始函数的每个函数。

原因是这些内核函数期望从系统调用中调用,并带有指向用户缓冲区的指针,而您希望使用内核缓冲区中的参数调用它们。

sem_timedwait() 为例——相关的内核函数是 ipc/sem.c 中的sys_timedwait()(参见此处:http://lxr.free-electrons.com/source/ipc/sem.c#L1537)。如果你在你的内核代码中复制这个函数并且只删除执行copy_from_user()copy_to_user() 的部分并简单地使用传递的指针(因为你将从内核空间调用它们),你将获得内核等效函数,可以从内核空间和用户空间获取 SysV 信号量 - 只要你从内核中的进程上下文调用它们(如果你不知道最后一句话是什么意思,我强烈推荐阅读 Linux Device Drivers, 3rd edition)。

祝你好运。

【讨论】:

这看起来很有希望。 内核中的用户上下文,您是指作为系统调用的结果运行的代码(例如在/sys 文件处理程序或ioctl 中)还是any kernel code that is not in interrupt context?换句话说,如果你有一个普通的内核模块,__init 函数是否在内核中的用户上下文中? kthread 中的代码怎么样? 我在这本书中搜索了user context 并没有看到太多。当我有更多时间时,我会进一步研究它。与此同时,如果你能把它弄清楚一点就好了。 @shahbaz 我的意思是任何不在中断上下文中的内核代码。我可能应该使用术语:“过程上下文”。很抱歉造成混乱。 好的,那太好了。我周末试试。希望 sysV 信号量的实现是稳定的,所以我不必关心内核版本,但这是我必须自己检查的事情。 @Shahbaz 很抱歉让你失望了,但是 SysV IPC 刚刚在 3.10 中发生了变化……Linux 内核 internals 永远不应该被认为是稳定的。【参考方案2】:

我能想到的一个解决方案是在主内核模块上有一个/proc(或/sys或其他)文件,在其中写入0/1(或读取/写入)会导致它在semaphore 上发出up/down。导出该信号量允许其他内核模块直接访问它,而用户应用程序将通过 /proc 文件系统。

我仍然会等待看原始问题是否有答案。

【讨论】:

不幸的是,我认为这不会起作用,因为 procfssysfs 操作是非原子的,并且会使程序容易受到竞争条件的影响。 @VilhelmGray,在虚拟文件的读/写处理程序中,我会调用updown。如果两个进程同时尝试单独读取/proc 文件,是否会出现问题? (例如,如果文件根本没有做任何事情?)如果没有,那么不应该有任何竞争条件,因为实际的同步是由运行良好的 up/down 函数完成的。 对不起,我误解了你的方法:我以为你正在阅读0/1,然后在用户空间中调用up/down。如果您完全从内核空间处理信号量 - 并且仅使用 /proc 文件来请求控制 - 那么您应该没有问题。 我最终选择了 sysfs,因为我觉得它更舒服,但 ioctl 也可以。 仅供参考,我上次尝试过(内核 3.something),这里有个问题:***.com/q/28052643/912144【参考方案3】:

无论如何我都没有这方面的经验,但这是我的看法。如果您查看 glibc 对 sem_open 和 sem_wait 的实现,它实际上只是在 /dev/shm 中创建一个文件,从中映射一个结构,并在其上使用原子操作。如果您想从用户空间访问命名信号量,您可能需要修补 tmpfs 子系统。但是,我认为这会很困难,因为要确定文件是否是命名信号量并不容易。

一种更简单的方法可能是重用内核的信号量实现并让内核管理用户空间进程的信号量。为此,您将编写一个与设备文件相关联的内核模块。然后为设备文件定义两个ioctl,一个用于等待,一个用于发布。这是编写内核模块的好教程,包括设置设备文件并为其添加 I/O 操作。 http://www.freesoftwaremagazine.com/articles/drivers_linux。我不确切知道如何实现 ioctl 操作,但我认为您可以将函数分配给 file_operations 结构的 ioctl 成员。不确定函数签名应该是什么,但您可能可以通过在内核源代码中挖掘来弄清楚。

【讨论】:

感谢您提供指向 glibc 实现的指针。您的建议与我自己的答案非常相似(除了使用ioctl 而不是sysfs)。我会考虑一下。【参考方案4】:

我相信您知道,即使是最好的解决方案也可能非常难看。如果我在你的位置,我会简单地认输并使用集合点来同步进程

【讨论】:

这是一个避免实施适当解决方案的糟糕理由。事实上,一个可行的解决方案甚至可能并不那么难以实施。就在我的脑海中,我可以想到一个可能的优雅解决方案:也许使用mmap 在用户和驱动程序之间共享内存,双方可以使用相同的内存来读取和写入原子值(即与信号量交互)。【参考方案5】:

我已经阅读了您项目的README 并且我有以下观察结果。提前道歉:

首先,实时系统已经有了一个通用接口。它被称为POSIX;当然 VxWorks、Integrity 和 QNX 是 POSIX 兼容的,根据我的经验,如果您在 POSIX API 中开发,那么可移植性问题很少。 POSIX 是否健全是另一回事,但它是我们都使用的。

[大多数 RTOS 符合 POSIX 的原因是因为它们的大市场之一是国防设备。而且美国国防部不会让您将操作系统用于他们的非 IT 设备(例如雷达),除非它符合 POSIX 标准……这几乎使得在没有 POSIX 的情况下开发 RTOS 在商业上是不可能的]

其次,通过应用PREMPT_RT 补丁集,Linux 本身可以成为一个非常好的实时操作系统。从有效利用所有这些多核 CPU 的角度来看,这可能是目前所有 RTOS 中最好的一个。然而,它不像其他操作系统那样硬实时操作系统,所以它是交换条件。

RTAI 采用了一种不同的方法,实际上将他们自己的 RTOS 置于 Linux 之下,并使 Linux 只不过是在其操作系统中运行的一项任务。这种方法在一定程度上是可以的,但 RTAI 的最大损失是实时位现在(据我所知) POSIX 兼容(尽管 API 看起来他们已经只是将 rt_ 放在一些 POSIX 函数名称的前面),现在与其他事物的交互正如您所发现的那样非常复杂。

PREEMPT_RT 是一个比 RTAI 更具侵入性的补丁集,但回报是其他一切(如 POSIX 和 valgrind)都保持完全正常。另外还有像 FTrace 这样的好东西。簿记就是仅使用现有工具的情况,而不必编写新工具。此外,看起来 PREEMPT_RT 正在逐渐进入主流 Linux 内核。这将使 RTAI 等其他补丁集变得毫无意义。

所以 Linux + PREEMPT_RT 为我们提供了实时 POSIX 以及一堆工具,就像所有其他 RTOS 一样;全面的共性。这听起来像是您项目的目标。

对于没有帮助您解决项目的“如何”问题,我深表歉意,我质疑“为什么”是非常不礼貌的行为。也是。但我觉得重要的是要知道有一些既定的事情似乎与你正在尝试做的事情有很大的重叠。取消 King POSIX 将会很困难。

【讨论】:

我知道您所说的大部分内容,但感谢您的努力。首先,关于 PREEMPT_RT,正如您所说,“它不是很实时”,在实时世界中翻译为“非实时”。也许一般来说这样的系统会很好,但如果没有任何保证,它就不能依赖于关键应用程序。 关于 POSIX,我知道它无处不在,但我的方式存在一些问题。首先,我需要以一种或另一种方式将 RTAI 安装在某个地方,但它们不能很好地协同工作(RTAI 基本上是 Linux 的唯一真正实时功能)。其次,POSIX 是非常不规则的,例如,有一种完全不同的方式来制作共享信号量和共享读写器锁。一个漂亮而干净的 API 可能很有吸引力(至少对我自己而言)。 第三,有些功能我宁愿立即使用。例如:get_and_reserve_an_unused_name()。目前,大多数实时人员在实践中处理非常基本的问题。他们的系统是高度静态和小型的,他们不考虑错误恢复等。POSIX 可能很好。我试图用 URT 实现的是构建一个允许更多动态和容错应用程序的子结构。如果你愿意,可以称之为未来主义。 @Shahbaz,听起来您已经考虑了很多!我目前正在使用 PREEMPT_RT,这很值得。我没有使用过 RTAI,尽管他们的方法可以很好地控制调度,所以如果它是一个更可靠、更快速的调度器,我并不感到惊讶。 PREEMPT_RT 确实不错,而且我认为它在最大延迟等方面与 RTAI 相差不远。是的,POSIX 很笨重,但我们就是这样。有。做新事物的风险在于它会成为支持的噩梦。对于像我这样的人(应用程序开发人员),使用 POSIX 以外的东西是一个很大的风险。【参考方案6】:

我想以不同的方式回答这个问题:你不想这样做。没有接口来做这种事情是有充分理由的,并且所有其他内核子系统都被设计和实现为永远不需要在用户和内核空间之间共享锁,这是有充分理由的。如果您开始使用可能会阻止内核执行某些操作的用户空间,那么锁顺序的复杂性和意外位置的隐式锁定将很快失控。

让我回想一下大约 15 年前我进行的一次很长的调试会议,至少可以阐明您可能遇到的复杂问题。我参与了一个文件系统的开发,其中大部分代码都在用户空间中。 FUSE 之类的东西。

内核将执行文件系统操作,将其打包成消息并将其发送到用户级守护程序并等待回复。用户态守护进程读取消息,执行操作并向内核写入回复,内核唤醒并继续操作。简单的概念。

关于文件系统,您需要了解的一件事是锁定。当您查找文件名时,例如“foo/bar”,内核以某种方式获取目录“foo”的节点,然后锁定它并询问它是否有文件“酒吧”。文件系统代码以某种方式找到“bar”,锁定它,然后解锁“foo”。锁定协议非常简单(除非您正在重命名),父母总是在孩子之前被锁定,而孩子在父母锁被释放之前被锁定。该文件的查找消息是在目录仍被锁定时发送到我们的用户级守护程序的内容,当守护程序回复时,内核将继续首先锁定“bar”,然后解锁“foo”。

我什至不记得我们正在调试的症状,但我记得这个问题不是很容易重现的,它需要数小时的文件系统折磨程序才能显现出来。但几周后,我们弄清楚了发生了什么。假设我们文件的完整路径是“/a/b/c/foo/bar”。我们正在查找“bar”,这意味着我们正在锁定“foo”。守护进程是一个普通的用户态进程,因此它所做的一些操作可以阻塞并且也可以被抢占。它实际上是通过网络通话,所以它可以阻塞很长时间。当我们等待用户态守护进程时,其他一些进程出于某种原因想要查找“foo”。为此,它拥有“c”的节点,当然是锁定的,并要求它查找“foo”。它设法找到它并尝试锁定它(它必须在我们释放“c”上的锁之前被锁定)并等待“foo”上的锁被释放。另一个进程想要查找“c”,它当然会在持有“b”上的锁的同时等待那个锁。另一个进程等待“b”并持有“a”。还有一个进程想要“a”并持有“/”上的锁。

这不是问题,还不是。这有时也会发生在正常的文件系统中,锁可以一直级联到根目录,您等待一段时间以等待一个慢速磁盘,磁盘响应,拥塞缓解,每个人都得到他们的锁,一切都运行良好。但在我们的例子中,长时间持有锁的原因是因为我们的分布式文件系统的远程服务器没有响应。 X 秒后,userland 守护程序超时,就在响应内核“bar”上的查找操作失败之前,它会向 syslog 记录一条带有时间戳的消息。时间戳需要的其中一件事是时区信息,所以它需要打开“/etc/localtime”,当然要这样做,它需要开始查找“/etc”,为此它需要锁定“/ ”。 “/”已经被其他人锁定,因此用户态守护进程等待其他人解锁“/”,而其他人则通过 5 个进程链等待守护进程响应。系统最终陷入完全死锁。

现在,也许您的代码不会出现这样的问题。您说的是实时系统,因此您可能拥有普通内核所没有的控制级别。但是我不确定添加一个意想不到的锁定复杂层是否会让您保持系统的实时属性,或者真的确保您在用户空间中所做的任何事情都不会造成死锁级联。如果您不分页,如果您从不接触任何文件描述符,如果您从不进行内存操作以及我现在无法真正想到的其他一些事情,您可以摆脱在用户空间和内核之间共享的锁,但是这会很困难,你可能会发现意想不到的问题。

【讨论】:

感谢您的回答,内容丰富。不过我相信,这不会发生。实时部分已经使用了可以在两个空间之间共享的 RTAI 锁定机制,我之前已经广泛使用它们并且那里没有问题。这个问题的锁定是针对软件的非实时簿记部分,它基本上总是采用以下形式:lock-access shared memory-unlock 所以唯一可能出现的问题是,如果我在关键部分中调用一个函数再次尝试锁定互斥锁,无论我是在用户空间和内核空间之间共享它还是它是只是其中之一。此外,内核空间部分与用户空间部分确实没有什么不同。没有中断处理或用户空间进程不能做的任何事情。唯一不同的是,就 RTAI 而言,同样的硬实时应用运行在内核空间中,实时操作的延迟更小。 好吧,我不知道你的具体情况。你可能会侥幸逃脱。我只是想画一幅画,为什么这通常不会完成。请记住,持有锁的用户态进程可能会以内存管理锁(故障)告终,内核端可能会以其他意外锁(中断)告终。用户空间可能会崩溃(退出涉及大量锁)和转储内核(文件系统锁)、信号处理程序(更多意外锁定)等,同时记住解锁共享锁,这样您就不会在用户空间意外退出时阻塞内核。 我会重新设计通信协议以使用循环无锁缓冲区。或者一个普通的管道,或者类似的东西。但这只是我保守。 我不介意进行更好的设计,但我不明白如何使用循环缓冲区或管道管理简单的临界区访问。共享信号量实际上只是一个互斥体,具有基本的lock-access-unlock 模式。【参考方案7】:

Linux/GLIBC 中存在多种解决方案,但没有一个允许在用户和内核空间之间明确共享信号量。 内核提供了暂停线程/进程的解决方案,最有效的是 futex。以下是有关同步用户空间应用程序的当前实现的最新技术的一些详细信息。

SYSV 服务

Linux System V (SysV) 信号量是同名 Unix 操作系统的遗产。它们基于锁定/解锁信号量的系统调用。对应的服务有:

semget() 获取标识符 semop() 对信号量进行操作(例如递增/递减) semctl() 对信号量进行一些控制操作(例如销毁)

GLIBC(例如 2.31 版本)在这些服务之上不提供任何附加值。图书馆服务直接调用同名的系统调用。例如semop()(在sysdeps/unix/sysv/linux/semtimedop.c中)直接调用相应的系统调用:

int
__semtimedop (int semid, struct sembuf *sops, size_t nsops,
          const struct timespec *timeout)

  /* semtimedop wire-up syscall is not exported for 32-bit ABIs (they have
     semtimedop_time64 instead with uses a 64-bit time_t).  */
#if defined __ASSUME_DIRECT_SYSVIPC_SYSCALLS && defined __NR_semtimedop
  return INLINE_SYSCALL_CALL (semtimedop, semid, sops, nsops, timeout);
#else
  return INLINE_SYSCALL_CALL (ipc, IPCOP_semtimedop, semid,
                  SEMTIMEDOP_IPC_ARGS (nsops, sops, timeout));
#endif

weak_alias (__semtimedop, semtimedop)

现在,SysV 信号量(以及其他 SysV IPC,如共享内存和消息队列)已被视为已弃用,因为它们需要对每个操作进行系统调用,因此它们会通过系统调用减慢调用进程上下文切换。新应用程序应使用 GLIBC 提供的符合 POSIX 标准的服务。

POSIX 服务

POSIX 信号量基于快速用户互斥体 (FUTEX)。该原则包括增加/减少用户空间中的信号量计数器,只要没有争用,就使用原子操作。但是当存在争用时(多个线程/进程想要同时“锁定”信号量),一个 futex() 系统调用会在信号量被唤醒时唤醒等待中的线程/进程“解锁”或挂起等待信号量释放的线程/进程。从性能的角度来看,这与上述 SysV 服务相比有很大的不同,后者系统地需要系统调用来执行任何操作。 POSIX 服务是在 GLIBC 中针对用户空间部分操作(原子操作)实现的,只有在发生争用时才会切换到内核空间。

例如,在 GLIBC 2.31 中,锁定信号量的服务位于 nptl/sem_waitcommon.c。它检查信号量的值以使用原子操作(在 __new_sem_wait_fast() 中)递减它并调用 futex() 系统调用(在 __new_sem_wait_slow ()) 来挂起调用线程仅当信号量在尝试递减之前等于 0。

static int
__new_sem_wait_fast (struct new_sem *sem, int definitive_result)

[...]
  uint64_t d = atomic_load_relaxed (&sem->data);
  do
    
      if ((d & SEM_VALUE_MASK) == 0)
    break;
      if (atomic_compare_exchange_weak_acquire (&sem->data, &d, d - 1))
    return 0;
    
  while (definitive_result);
  return -1;
[...]

[...]
static int
__attribute__ ((noinline))
__new_sem_wait_slow (struct new_sem *sem, clockid_t clockid,
             const struct timespec *abstime)

  int err = 0;

[...]
  uint64_t d = atomic_fetch_add_relaxed (&sem->data,
      (uint64_t) 1 << SEM_NWAITERS_SHIFT);

  pthread_cleanup_push (__sem_wait_cleanup, sem);

  /* Wait for a token to be available.  Retry until we can grab one.  */
  for (;;)
    
      /* If there is no token available, sleep until there is.  */
      if ((d & SEM_VALUE_MASK) == 0)
    
      err = do_futex_wait (sem, clockid, abstime);
[...]

基于futex的POSIX服务举例:

sem_init() 创建信号量 sem_wait() 锁定信号量 sem_post() 解锁信号量 sem_destroy() 销毁信号量

要管理互斥体(即二进制信号量),可以使用 pthread 服务。它们也是基于 futex 的。例如:

pthread_mutex_init() 创建/初始化互斥体 pthread_mutex_lock/unlock() 锁定/解锁互斥锁 pthread_mutex_destroy() 销毁互斥锁​​

【讨论】:

我通读了您的答案,但没有找到关于用户和内核空间之间“如何共享”信号的信息。我是否遗漏了什么,或者您试图表明 SysV 已被弃用,因为它需要系统调用? @LouisGo:你没有错过任何东西。我想指出一个事实,即 Linux/GLIBC 中已经存在多种解决方案,但没有一个允许在用户和内核之间明确共享信号量。内核提供了暂停线程/进程的解决方案,最有效的是 futex。 你能把结论放在答案的开头吗?这将有助于未来的用户。根据您的描述,任何使用 futex 的技术都不允许用户空间和内核之间进行通信。所以试图找到解决方案的人会知道 futex 在这个用例中不起作用。【参考方案8】:

我正在考虑内核和用户空间直接共享事物的方式,即无需系统调用/复制输出成本。我记得的一件事是 RDMA 模型,其中内核直接从用户空间写入/读取,当然还有同步。您可能想探索该模型,看看它是否适合您的目的。

【讨论】:

可能,但可能更困难的部分是用户空间应用程序如何锁定内核空间应用程序。 IIRC,有一些关于门铃、注册回调等同步的东西。您可能需要深入了解这一点。 有趣。我一定会调查的。 RDMA 是否代表 Remote DMA?***说这是从一台计算机的内存直接访问另一台计算机的内存。这是你建议我调查的吗?还是另一个 RDMA?

以上是关于用户和内核空间之间的共享信号量的主要内容,如果未能解决你的问题,请参考以下文章

如何从内核向用户空间发送信号

linux下怎么实现内核态和用户空间进程共享内存

在 Linux 内核命名空间之间使用 POSIX 信号量

进程的内核态,用户态以及信号

Binder机制概述

linux下通过shmget创建的共享内存,是属于用户空间还是内核空间?