glibc如何处理clone()?
通过特定于体系结构的汇编包装器。对于i386,请参见glibc源代码中的sysdeps/unix/sysv/linux/i386/clone.S;对于x86-64,请参见sysdeps/unix/sysv/linux/x86-64/clone.S,以此类推。
普通的系统调用包装器是不足够的,因为用户空间需要切换堆栈。上述汇编文件有非常详细的注释,说明除了系统调用外,在用户空间实际上需要做什么。
所有内核中系统调用的参数与glibc中的clone并不完全相同,那么这些变化是如何处理的?
映射到系统调用的C库函数是包装函数。
例如,考虑POSIX.1 C库低级I/O函数
write()
和Linux
write()
系统调用。参数基本相同,错误条件也相同,但错误返回值不同。如果发生错误,C库函数返回
-1
并设置
errno
,而Linux系统调用返回负错误代码(基本匹配
errno
值)。
如果您查看例如
sysdeps/unix/sysv/linux/x86_64/sysdep.h,您会发现Linux x86-64的基本系统调用包装器简化为:
# 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
#else
#if defined(__i386)
#define __NR_splork
#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) {
errno = -retval;
return -1;
} else
return (int)retval;
}
#else
#undef SYS_splork
int splork(void *arg2, unsigned long arg1)
{
errno = ENOTSUP;
return -1;
}
#endif
#endif
SYS_splork
和__NR_splork
是预处理宏,定义了新系统调用的系统调用号。由于该系统调用号可能尚未(?)包含在官方内核源代码和头文件中,因此上述头文件为每个支持的架构显式声明了它。对于不支持它的体系结构,splork()
函数将始终返回-1
,并带有errno == ENOTSUP
。
但请注意,Linux系统调用仅限于6个参数。如果您的内核函数需要更多参数,则需要将参数打包到一个结构中,将该结构的地址传递给内核,并使用copy_from_user()
将值复制到内核中的同一结构中。
在所有Linux体系结构中,指针和long
大小相同(int
可能小于指针),因此我建议您在这些结构中使用long
或固定大小类型来传递数据以及从内核中获取数据。