【问题标题】:How to implement another variation of clone(2) syscall in linux kernel?如何在 linux 内核中实现 clone(2) 系统调用的另一种变体?
【发布时间】:2017-04-20 15:40:51
【问题描述】:

我正在尝试创建另一个版本的 clone(2) 系统调用(在内核空间中)以创建具有一些附加参数的用户进程的克隆。此系统调用将执行与 clone(2) 完全相同的工作但我想从 user_space 向内核传递一个额外的参数。但是当我看到 glibc 的code 似乎每个参数的传递顺序都与用户调用 clone() 的顺序不同

int clone(int (*fn)(void *), void *child_stack,
             int flags, void *arg, ...
             /* pid_t *ptid, void *newtls, pid_t *ctid */ );

其中一些是由 glibc 的代码本身处理的。我在互联网上搜索以了解 glib 的 clone() 是如何工作的,但找不到更好的文档。 谁能解释一下

  1. glibc 如何处理 clone()?
  2. 而且内核中syscall的所有参数都与glibc中的clone不完全相同,那么这些变化是如何处理的呢?

【问题讨论】:

    标签: c linux fork system-calls glibc


    【解决方案1】:

    glibc 如何处理 clone()?

    通过特定于架构的程序集包装器。对于 i386,请参阅 glibc 源代码中的sysdeps/unix/sysv/linux/i386/clone.S;对于 x86-64,请参阅 sysdeps/unix/sysv/linux/x86-64/clone.S 等。

    普通的系统调用包装器是不够的,因为切换堆栈取决于用户空间。除了系统调用之外,上述程序集文件包含有关在用户空间中实际需要执行的操作的大量信息。


    kernel中syscall的所有参数都和glibc中的clone不完全一样,那么这些变异是如何处理的呢?

    映射到系统调用的 C 库函数是包装函数。

    以 POSIX.1 write() C 库低级 I/O 函数和 Linux write() 系统调用为例。参数基本相同,错误条件也基本相同,但错误返回值不同。如果发生错误,C 库函数返回 -1 并设置 errno,而 Linux 系统调用返回负错误代码(基本匹配 errno 值)。

    如果您查看例如sysdeps/unix/sysv/linux/x86_64/sysdep.h,您可以看到 x86-64 上 Linux 的基本系统调用包装器归结为

    # define INLINE_SYSCALL(name, nr, args...) \
      ({                                       \
        unsigned long int resultvar = INTERNAL_SYSCALL (name, , nr, args);        \
        if (__glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (resultvar, )))            \
          {                                                                       \
            __set_errno (INTERNAL_SYSCALL_ERRNO (resultvar, ));                   \
            resultvar = (unsigned long int) -1;                                   \
          }                                                                       \
        (long int) resultvar; })
    

    它只调用实际的系统调用,然后检查系统调用返回值是否指示错误;如果是,则将结果更改为-1 并相应地设置errno。它只是看起来很有趣,因为它依赖 GCC 扩展使其表现得像单个语句。


    假设您向 Linux 添加了一个新的系统调用

    SYSCALL_DEFINE2(splork, unsigned long, arg1, void *, arg2);
    

    并且,无论出于何种原因,您希望将其公开给用户空间

    int splork(void *arg2, unsigned long arg1);
    

    没问题!您只需要提供一个最小的头文件,

    #ifndef _SPLORK_H
    #define _SPLORK_H
    #define _GNU_SOURCE
    #include <sys/syscall.h>
    #include <errno.h>
    
    #ifndef __NR_splork
    #if defined(__x86_64__)
    #define __NR_splork /* syscall number on x86-64 */
    #else
    #if defined(__i386)
    #define __NR_splork /* syscall number on i386 */
    #endif
    #endif
    
    #ifdef __NR_splork
    #ifndef SYS_splork
    #define SYS_splork __NR_splork
    #endif
    
    int splork(void *arg2, unsigned long arg1)
    {
        long retval;
    
        retval = syscall(__NR_splork, (long)arg1, (void *)arg2);
        if (retval < 0) {
            /* Note: For backward compatibility, we might wish to use
                         *(__errno_location()) = -retval;
                     here. */
            errno = -retval;
            return -1;
        } else
            return (int)retval;
    }
    
    #else
    #undef SYS_splork
    
    int splork(void *arg2, unsigned long arg1)
    {
        /* Note: For backward compatibility, we might wish to use
                     *(__errno_location()) = ENOTSUP;
                 here. */
        errno = ENOTSUP;
        return -1;
    }
    
    #endif
    
    #endif /* _SPLORK_H */
    

    SYS_splork__NR_splork 是定义新系统调用的系统调用号的预处理器宏。由于系统调用号可能(还没有?)包含在官方内核源代码和头文件中,因此上述头文件为每个支持的架构明确声明了它。对于不支持的架构,splork() 函数将始终返回 -1errno == ENOTSUP

    但是请注意,Linux 系统调用仅限于 6 个参数。如果您的内核函数需要更多,您需要将参数打包到一个结构中,将该结构的地址传递给内核,然后使用copy_from_user() 将值复制到内核中的同一结构中。

    在所有 Linux 架构中,指针和long 的大小相同(int 可能小于指针),因此我建议您在此类结构中使用long 或固定大小的类型将数据传递给/来自内核。

    【讨论】:

      猜你喜欢
      • 2014-01-27
      • 1970-01-01
      • 2011-11-16
      • 1970-01-01
      • 1970-01-01
      • 2011-12-15
      • 2014-03-20
      • 2013-11-01
      • 1970-01-01
      相关资源
      最近更新 更多