系统调用
操作系统为在用户态运行的进程与硬件设备进行交互提供了一组接口。这说明了系统调用的接口是操作系统提供的。
系统调用与libc库的关系
unix系统给程序员提供了很多API的库函数。libc的标准C库所定义的一些API引用了封装例程(wrapper
routine)(其唯一目的就是发布系统调用)。通常情况下,每个系统调用对应一个封装例程,而封装例程定义了应用程序使用的API【是不是说,有些系统调用没有使用封装例程?】。
反之则不然,一个API没必要对应一个特定的系统调用。首先,API可以直接提供用户态的服务(例如一些抽象的数学函数,根本没必要使用系统调用)。其次,一个单独的API可能调用了几个系统调用。此外,几个API函数可能调用封装不同功能的同一系统调用。
POSIX标准
POSIX标准针对API而不是针对系统调用。判断一个系统是否与posix兼容要看它是否提供一组合适的应用程序接口,而不管对应的函数是如何实现的。
系统调用处理程序及服务例程
当用户态的进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。
在80x86体系结构中,可以使用两种不同的方式调用Linux的系统调用。两种方式的最终结果都是跳转到所谓系统调用处理程序(system call handler)的汇编语言函数。
-
内核如何区分不同的系统调用?
因为内核实现了很多不同的系统调用,因此进程必须传递一个名为系统调用号(system call number)的参数来识别所需的系统调用,eax寄存器就用做此目的。【因此,每个系统调用至少有一个参数,即通过eax寄存器传递来的系统调用号。】 -
系统调用号和系统调用服务例程如何关联?
为了把系统调用号与相应的服务例程关联起来,内核利用了一个系统分派表(dispatch table)。有点类似中断号和中断服务程序的意思。 -
系统调用的返回值?
所有的系统调用都返回一个整数值。这些返回值与封装例程的约定是不同的。在内核中,正数或0表示系统调用成功结束,而负数表示一个出错条件。在后一种情况下,这个值就是存放在errno变量中必须返回给应用程序 负出错码。内核没有设置或使用errno变量,而封装例程从系统调用返回之后设置这个变量。
系统调用处理程序的处理过程?
- 在内核态栈保存大多数寄存器的内容(这个操作对所有的系统调用都是通用的,并用汇编语言编写)。
- 调用名为系统调用服务例程(system call service routine)的相应的C函数来处理系统调用。
- 退出系统调用处理程序:用保存在内核栈中的值加载寄存器,CPU从内核态切换到 用户态(所有的系统调用都要执行这一相同的操作,该操作用汇编语言代码实现)。
进入和退出系统调用
本地应用可以通过两种不同的方式调用系统调用:
- 执行int $0x80汇编语言指令。在Linux内核的老版本中,这是从用户态切换到内核态的唯一方式。
- 执行sysenter汇编语言指令。Linux2.6内核支持这条指令。
同样,内核可以通过两种方式从系统调用退出,从而使CPU切换到用户态:
- 执行iret汇编语言指令.
- 执行sysexit汇编语言指令.
参数传递
与普通函数类似,系统调用通常也需要输入\输出参数,这些参数可是是实际的值(例如数值),也可能是用户态进程地址空间的变量,甚至是指向用户态函数的指针的数据地址地址结构。
fork()系统调用并不需要其他的参数。
普通的C函数的参数传递是通过把参数值写入活动的程序栈(用户态栈或者内核态栈)实现的。因为系统调用时一种横跨用户和内核两大陆地的特殊函数,所以既不能使用用户态栈也不能使用内核态栈。更确切地说,在发出系统调用之前,系统调用的参数被写入到CPU寄存器中,然后在调用系统调用服务例程(系统调用的实际处理程序)之前,内核再把存放在CPU中的参数拷贝到内核态堆栈中,这是因为系统调用服务例程是普通的C函数。
为什么内核不直接把参数从用户态 栈拷贝到内核态的栈呢?首先,同时操作两个栈是比较复杂的。其次 ,寄存器的使用使得系统调用处理程序的结构和其他异常处理程序的结构类似。
验证参数
在内核打算满足用户的请求之前,必须仔细地检查所有的系统调用参数。检查的类型既依赖于系统调用,也依赖于特定的参数。只要一个参数指定的是地址,内核必须检查它是否在这个进程的地址空间之内。有两种可能的方式来执行这种检查:
- 验证这个线性地址是否属于这个进程的地址空间,如果是,这个线性地址所在的线性区就具有正确的访问权限。【费时】
- 仅仅验证这个线性地址是否小于PAGE_OFFSET(即没有落在内核的线性地址区间内)。【粗略但高效】
验证线性地址小于PAGE_OFFSET是判断它的有效性的必要条件而不是充分条件。
为什么要进行这种粗略检查?
- 这种粗略检查是至关重要的,它确保了进程地址空间和内核地址空间都不被非法访问。【RAM的映射是从PAGE_OFFSET开始的。这就意味着内核例程能对内存中现有的所有页进行寻址。因此,如果不进行 这种粗略检查,用户态进程就可能把属于内核地址空间的一个地址作为参数来传递,然后还能对内存中现有的任何页进行读写而不引起缺页异常。】
- 对系统调用所传递地址的检查是通过access_ok()宏实现的。
访问进程地址空间
系统调用服务例程需要非常频繁地读写进程地址空间的数据。Linux包含的一组宏是这种访问更加容易。get_user()宏从一个地址读取1、2或4个连续字节;put_user()宏把这几种大小的内容写入一个地址中。
了解几个内核访问进程地址空间的函数和宏,便于阅读和理解内核源码。
动态地址检查:修正代码
上面检查系统调用参数指定的地址是否小于PAGE_OFFSET只能保证用户态进程不会试图侵扰内核地址空间。但是,由参数传递的线性地址依然可能不属于进程地址空间。在这种情况下,当内核试图使用任何这种错误地址时,将会发生缺页异常。
在描述内核如何检测这种错误之前,我们先说明一下在内核态引起缺页 异常的四中情况。这些情况必须由缺页异常处理程序来区分,因为不同情况采取的操作很大不同:
1.内核试图访问属于进程地址空间的页,但是,相应的页框不存在或者是内核试图去写一个只读页。在这些情况下,处理程序必须分配和初始化一个新的页框。【缺页异常程序–在80x86上就是do_page_fault()】
2.内核寻址到属于其地址的页,但是相应的页表项还没有初始化。在这种情况下,内核必须在当前进程页表中适当的建立一些表项。
3.某一内核函数包含编程错误,当这个函数运行时就引起异常(就是我们常说的内核bug);或者可能是由于瞬间的硬件错误引起异常。当 这种情况发生时,处理程序必须执行一个内核漏洞。
4.本章所讨论的一种情况:当系统调用服务例程试图读写一个内存区,而该内存区的地址是通过系统调用参数传递来的,但却不属于进程的地址空间。
内核封装例程
内核封装例程对应于libc库中的封装例程。尽管系统调用主要是有用户态进程使用,但是也可以被内核线程使用,但是内核线程不是使用libc库函数。
为了简化相应封装例程的声明,Linux定义了7个从_syscall0到_syscall6的一组宏。每个宏名字中的数字0~6对应着系统调用所用的参数个数(系统调用号除外)。然而,不能用这些宏来为超过6个参数(系统调用号除外)的系统调用或产生非标准返回值的系统调用来定义封装例程。
==================================================================================
系统调用号
unistd.h是 C 和 C++ 程序设计语言中提供对 POSIX 操作系统 API 的访问功能的头文件的名称。
linux-3.18.132\include\uapi\asm-generic\unistd.h
/* ipc/mqueue.c */
#define __NR_mq_open 180
__SC_COMP(__NR_mq_open, sys_mq_open, compat_sys_mq_open)
#define __NR_mq_unlink 181
__SYSCALL(__NR_mq_unlink, sys_mq_unlink)
#define __NR_mq_timedsend 182
__SC_COMP(__NR_mq_timedsend, sys_mq_timedsend, compat_sys_mq_timedsend)
#define __NR_mq_timedreceive 183
__SC_COMP(__NR_mq_timedreceive, sys_mq_timedreceive, \
compat_sys_mq_timedreceive)
#define __NR_mq_notify 184
__SC_COMP(__NR_mq_notify, sys_mq_notify, compat_sys_mq_notify)
#define __NR_mq_getsetattr 185
__SC_COMP(__NR_mq_getsetattr, sys_mq_getsetattr, compat_sys_mq_getsetattr)
/* ipc/msg.c */
#define __NR_msgget 186
__SYSCALL(__NR_msgget, sys_msgget)
#define __NR_msgctl 187
__SC_COMP(__NR_msgctl, sys_msgctl, compat_sys_msgctl)
#define __NR_msgrcv 188
__SC_COMP(__NR_msgrcv, sys_msgrcv, compat_sys_msgrcv)
#define __NR_msgsnd 189
__SC_COMP(__NR_msgsnd, sys_msgsnd, compat_sys_msgsnd)
/* ipc/sem.c */
#define __NR_semget 190
__SYSCALL(__NR_semget, sys_semget)
#define __NR_semctl 191
__SC_COMP(__NR_semctl, sys_semctl, compat_sys_semctl)
#define __NR_semtimedop 192
__SC_COMP(__NR_semtimedop, sys_semtimedop, compat_sys_semtimedop)
#define __NR_semop 193
__SYSCALL(__NR_semop, sys_semop)
/* ipc/shm.c */
#define __NR_shmget 194
__SYSCALL(__NR_shmget, sys_shmget)
#define __NR_shmctl 195
__SC_COMP(__NR_shmctl, sys_shmctl, compat_sys_shmctl)
#define __NR_shmat 196
__SC_COMP(__NR_shmat, sys_shmat, compat_sys_shmat)
#define __NR_shmdt 197
__SYSCALL(__NR_shmdt, sys_shmdt)
系统调用处理程序
COMPAT_SYSCALL_DEFINE4
SYSCALL_DEFINE4
/*path--ipc/compat_mq.c*/
COMPAT_SYSCALL_DEFINE4(mq_open, const char __user *, u_name,
int, oflag, compat_mode_t, mode,
struct compat_mq_attr __user *, u_attr)
{
void __user *p = NULL;
if (u_attr && oflag & O_CREAT) {
struct mq_attr attr;
memset(&attr, 0, sizeof(attr));
p = compat_alloc_user_space(sizeof(attr));
if (get_compat_mq_attr(&attr, u_attr) ||
copy_to_user(p, &attr, sizeof(attr)))
return -EFAULT;
}
return sys_mq_open(u_name, oflag, mode, p);
}
/*path--ipc/mqueue.c*/
SYSCALL_DEFINE4(mq_open, const char __user *, u_name, int, oflag, umode_t, mode,
struct mq_attr __user *, u_attr)
{
struct path path;
struct file *filp;
struct filename *name;
struct mq_attr attr;
int fd, error;
struct ipc_namespace *ipc_ns = current->nsproxy->ipc_ns;
struct vfsmount *mnt = ipc_ns->mq_mnt;
struct dentry *root = mnt->mnt_root;
int ro;
if (u_attr && copy_from_user(&attr, u_attr, sizeof(struct mq_attr)))
return -EFAULT;
audit_mq_open(oflag, mode, u_attr ? &attr : NULL);
if (IS_ERR(name = getname(u_name)))
return PTR_ERR(name);
fd = get_unused_fd_flags(O_CLOEXEC);
if (fd < 0)
goto out_putname;
ro = mnt_want_write(mnt); /* we'll drop it in any case */
error = 0;
mutex_lock(&root->d_inode->i_mutex);
path.dentry = lookup_one_len(name->name, root, strlen(name->name));
if (IS_ERR(path.dentry)) {
error = PTR_ERR(path.dentry);
goto out_putfd;
}
path.mnt = mntget(mnt);
if (oflag & O_CREAT) {
if (path.dentry->d_inode) { /* entry already exists */
audit_inode(name, path.dentry, 0);
if (oflag & O_EXCL) {
error = -EEXIST;
goto out;
}
filp = do_open(&path, oflag);
} else {
if (ro) {
error = ro;
goto out;
}
audit_inode_parent_hidden(name, root);
filp = do_create(ipc_ns, root->d_inode,
&path, oflag, mode,
u_attr ? &attr : NULL);
}
} else {
if (!path.dentry->d_inode) {
error = -ENOENT;
goto out;
}
audit_inode(name, path.dentry, 0);
filp = do_open(&path, oflag);
}
if (!IS_ERR(filp))
fd_install(fd, filp);
else
error = PTR_ERR(filp);
out:
path_put(&path);
out_putfd:
if (error) {
put_unused_fd(fd);
fd = error;
}
mutex_unlock(&root->d_inode->i_mutex);
if (!ro)
mnt_drop_write(mnt);
out_putname:
putname(name);
return fd;
}
Linux内核错误码位置
linux-3.18.132\include\uapi\asm-generic\errno-base.h
linux-3.18.132\include\uapi\asm-generic\errno.h
glibc库源码
__NR_mq_open
#ifdef __NR_mq_open
/* Establish connection between a process and a message queue NAME and
return message queue descriptor or (mqd_t) -1 on error. OFLAG determines
the type of access used. If O_CREAT is on OFLAG, the third argument is
taken as a `mode_t', the mode of the created message queue, and the fourth
argument is taken as `struct mq_attr *', pointer to message queue
attributes. If the fourth argument is NULL, default attributes are
used. */
mqd_t
__mq_open (const char *name, int oflag, ...)
{
if (name[0] != '/')
{
__set_errno (EINVAL);
return -1;
}
mode_t mode = 0;
struct mq_attr *attr = NULL;
if (oflag & O_CREAT)
{
va_list ap;
va_start (ap, oflag);
mode = va_arg (ap, mode_t);
attr = va_arg (ap, struct mq_attr *);
va_end (ap);
}
return INLINE_SYSCALL (mq_open, 4, name + 1, oflag, mode, attr);
}
strong_alias (__mq_open, mq_open);
#else
#include <rt/mq_open.c>
#endif
系统调用处理程序及服务例程
xyz()系统调用对应的服务例程的名字通常是sys_xyz()。不过也有一些例外。
进入和退出系统调用
总结
参考文献及源码
- http://ftp.gnu.org/gnu/glibc/glibc-2.18.tar.gz
- https://www.kernel.org/
- 《深入理解LINUX内核》陈莉君 张琼声 张宏伟 译