23

简洁版本

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

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

长版

我正在为实时系统开发一个统一的接口,在其中我有一些额外的簿记需要处理,并受信号量保护。这些簿记是在资源分配和释放上完成的,这是在非实时环境中完成的。

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

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

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

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

4

8 回答 8

15

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

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

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

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

因此,您的用户代码将简单地调用sem_timedwait(). 那是容易的部分。

内核部分有点棘手:您必须sem_timedwait()在内核中找到实现和相关调用的代码(它们都在文件 ipc/sem.c 中)并创建每个函数的副本原始函数在没有调用copy_from_user(...)andcopy_to_user(..)和 Friends 的情况下做了什么。

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

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

祝你好运。

于 2013-07-04T06:31:37.030 回答
6

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

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

于 2013-07-03T08:59:12.620 回答
3

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

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

于 2013-07-04T00:42:23.437 回答
2

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

于 2013-07-03T19:28:07.733 回答
2

我已阅读您项目的自述文件,并且有以下观察结果。提前致歉:

首先,实时系统已经有了一个通用接口。它被称为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 将是困难的。

于 2013-07-05T06:24:07.303 回答
2

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

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

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

关于文件系统,您需要了解的一件事是锁定。当您查找文件名时,例如“foo/bar”,内核以某种方式获取目录“foo”的节点,然后锁定它并询问它是否有文件“bar”。文件系统代码以某种方式找到“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 个进程链等待守护进程响应。系统最终完全陷入僵局。

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

于 2013-07-10T08:01:47.477 回答
1

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 服务例如:

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

于 2020-11-19T11:38:06.840 回答
0

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

于 2013-07-10T17:07:15.207 回答