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

时间:2013-06-30 14:19:01

标签: c linux-kernel semaphore

短版

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

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

长版

我正在开发一个unified interface to real-time systems,我在其中添加了一些书籍,由信号量保护。这些书籍保留是在资源分配和解除分配上完成的,这是在非实时环境中完成的。

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

我正在寻找的是一种在内核和用户空间之间共享正常Linux或POSIX信号量的方法,这样我就可以安全地在非实时环境中等待/发布它。

非常感谢有关此主题的任何信息。如果这是不可能的,你还有其他想法如何完成这项任务吗? 1

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

8 个答案:

答案 0 :(得分:14)

嗯,你的方向正确,但并不完全 -

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设备驱动程序,第3版)。

祝你好运。

答案 1 :(得分:5)

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

我还在等着看原问题是否有答案。

答案 2 :(得分:3)

我对此并没有任何经验,但这是我的看法。如果你看一下glibc的sem_opensem_wait的实现,它实际上只是在/ dev / shm中创建一个文件,从中创建一个结构,并在其上使用原子操作。如果要从用户空间访问命名信号量,则可能必须修补tmpfs子系统。但是,我认为这很难,因为确定一个文件是否是一个命名的信号量并不是直截了当的。

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

答案 3 :(得分:2)

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

答案 4 :(得分:2)

我已阅读您的项目README,我有以下观察。提前道歉:

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

[大多数RTOS符合POSIX的原因是因为其中一个主要市场是防御设备。并且美国国防部不会让你使用操作系统用于他们的非IT设备(例如雷达),除非它符合POSIX ......这几乎使得在没有给它POSIX的情况下做RTOS的商业上是不可能的]

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

RTAI采用了一种不同的方法,实际上将自己的RTOS置于Linux下,使Linux只能在其操作系统中运行一个任务。这种方法在某种程度上是可以接受的,但RTAI的一个重要影响是现在的实时位(据我所知)符合POSIX(尽管API看起来像他们一样)只是把rt_放在一些POSIX函数名称的前面),现在正如你所发现的那样,与其他东西的互动非常复杂。

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

所以Linux + PREEMPT_RT为我们提供了实时POSIX和一堆工具,就像那里的所有其他RTOS一样;全面共识。哪种听起来像是你项目的目标。

我为没有帮助你的项目“如何”而道歉,而且查询“为什么?”对我来说是非常不合理的。它也是。但我觉得重要的是要知道那里有确定的东西似乎与你想要做的事情有很大的重叠。取消国王POSIX将是困难的。

答案 5 :(得分:1)

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

让我回想一下我15年前做过的一段很长的调试时间,至少可以解释一下你可能遇到的复杂问题。我参与了开发一个文件系统,其中大部分代码都在用户区。像FUSE这样的东西。

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

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

我甚至不记得我们正在调试的症状,但我记得这个问题并非简单可重复,它需要数小时的文件系统折磨程序才能显现出来。但几个星期后,我们发现了正在发生的事情。假设我们文件的完整路径是“/ a / b / c / foo / bar”。我们正在对“bar”进行查找,这意味着我们正在锁定“foo”。守护程序是一个普通的用户空间进程,因此它执行的某些操作可以阻止并且也可以被抢占。它实际上是通过网络进行通话,因此可以阻止很长时间。当我们等待userland守护进程时,其他一些进程由于某种原因想要查找“foo”。要做到这一点,它有“c”的节点,当然是锁定的,并要求它查找“foo”。它设法找到它并尝试锁定它(它必须在我们可以释放锁定“c”之前被锁定)并等待“foo”上的锁被释放。另一个进程是想要查找“c”,它当然会在锁定“b”时等待锁定。另一个进程等待“b”并保持“a”。另一个过程想要“a”并持有“/".

的锁定

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

现在,也许你的代码不会有这样的问题。你在谈论一个实时系统,所以你可能有一定程度的控制,正常的内核没有。但我不确定是否添加一个意想不到的锁定复杂层甚至可以让你保留系统的实时属性,或者确保你在userland中做的任何事情都不会创建死锁级联。如果你没有页面,如果你从不接触任何文件描述符,如果你从未做过内存操作和其他一些我现在无法想到的事情,你可以放弃在userland和kernel之间共享的锁,但是这很难,你可能会发现意想不到的问题。

答案 6 :(得分:1)

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

SYSV服务

Linux System V(SysV)信号灯是同名Unix OS的遗留物。它们基于锁定/解锁信号量的系统调用。相应的服务是:

  • 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)。原理是只要没有争用,就可以使用原子操作在用户空间中增加/减少信号量计数器。但是,当发生争用时(多个线程/进程希望同时“锁定”信号量),在信号量为1时,将执行 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。例如:

答案 7 :(得分:0)

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