Linux系统调用

@


在linux中,内核空间和用户空间之间增加了一个中间层——系统调用层,如下图:
在这里插入图片描述

系统内调用层主要作用:

  • 为用户空间程序提供一层硬件抽象接口。
  • 保证系统稳定和安全,应用陈续访问内核必须通过系统调用层,那么内核可以在系统调用层对应用的访问权限、用户类型和其他一些规则进行过滤。
  • 可移植性,可以让应用程序在不修改源代码的情况下,重新编辑便可在不同操作系统或不同架构系统上运行。

系统调用和POSIX标准

一般来说,应用程序调用用户空间实现的编程接口来编程,而不是直接调用系统调用。一个API接口函数可以由一个系统调用实现,也可以由多个系统调用实现,或者完全不由系统调用实现。因此,一个API接口和特定的系统调用之间并无对应关系。

在UNIX世界中,最通用的系统调用层接口是POSIX(Portable Operating System Interface of UNIX)标准。POSIX标准针对的是API而不是系统调用,判断一个系统是否与POSIX兼容,要看它是否提供一组合适的应用编程接口,而不是看它的系统调用是如何定义和实现的。

Linux操作系统中的API接口通常是以C 标准库的方式提供,比如libc库。C库中提供了POSIX的绝大部分的API实现,同时也为内核提供的系统调用封装了相应的函数。

系统调用表

Linux系统为每个系统调用赋予了一个系统调用号,系统调用号一旦分配之后就不会有任何变更,否则编译好的应用程序就不能运行了。
在x86_64上,其系统调用号定义在arch/x86/include/generated/uapi/asm/unistd_64.h:

#ifndef _UAPI_ASM_UNISTD_64_H
#define _UAPI_ASM_UNISTD_64_H

#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
...
#define __NR_memfd_secret 447
#define __NR_process_mrelease 448
#define __NR_futex_waitv 449
#define __NR_set_mempolicy_home_node 450
#define __NR_cachestat 451
#define __NR_fchmodat2 452
#define __NR_map_shadow_stack 453

#ifdef __KERNEL__
#define __NR_syscalls 454
#endif

#endif /* _UAPI_ASM_UNISTD_64_H */

以open为例,其系统调用号为2,open函数最终的实现在fs/open.c

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
	if (force_o_largefile())
		flags |= O_LARGEFILE;
	return do_sys_open(AT_FDCWD, filename, flags, mode);
}

SYSCALL_DEFINE*是一组专门用来定义系统调用的宏,其实现在include/linux/syscalls.h

#ifndef SYSCALL_DEFINE0
#define SYSCALL_DEFINE0(sname)					\
	SYSCALL_METADATA(_##sname, 0);				\
	asmlinkage long sys_##sname(void);			\
	ALLOW_ERROR_INJECTION(sys_##sname, ERRNO);		\
	asmlinkage long sys_##sname(void)
#endif /* SYSCALL_DEFINE0 */

#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)

#define SYSCALL_DEFINE_MAXARGS	6

#define SYSCALL_DEFINEx(x, sname, ...)				\
	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

__SYSCALL_DEFINEx宏最后会扩展成__do_sys_open函数(在6.6.69版本是这样,较早的版本则不是,这个不是固定的)。

程序直接访问系统调用

可以通过syscall函数来直接访问系统调用而不需要通过C 标准库函数。

#define _GNU_SOURCE         /* See feature_test_macros(7) */
       #include <unistd.h>
       #include <sys/syscall.h>   /* For SYS_xxx definitions */

       long syscall(long number, ...);

下面测试下用syscall直接调用open系统调用:

#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
        int fd = syscall(SYS_open, "/tmp/call.log", O_CREAT);
        return 0;
}

运行程序后,可以看到成功创建了/tmp/call.log这个临时文件。

新增系统调用

Linux系统的系统调用必须由Linux社区来决定,并且和glibc社区同步,总之这个过程一般会很漫长。
为了实现用户程序和内核进行信息交互,除了系统调用,还可以使用下面的方法:

  • 设备节点。实现一个设备节点后,就可以对设备进行I/O操作了,也可以通过ioctl接口自定义一些操作
  • sysfs接口,可以让用户程序和内核直接通信,这种方式比较灵活,是linux内核推荐的方法

linux系统调用和库函数调用的区别

Linux下对文件操作有两种方式:系统调用(system call)和库函数调用(Library functions)。系统调用实际上就是指最底层的一个调用,在linux程序设计里面就是底层调用的意思,面向的是硬件。而库函数调用则面向的是应用开发的,相当于应用程序的api,采用这样的方式有很多种原因,第一:双缓冲技术的实现。第二,可移植性。第三,底层调用本身的一些性能方面的缺陷。第四:让api也可以有了级别和专门的工作面向。

系统调用

系统调用提供的函数如open, close,read, write, ioctl等,需包含头文件unistd.h。以write为例:其函数原型为 size_t write(int fd, const void *buf, size_t nbytes),其操作对象为文件描述符或文件句柄fd(file descriptor),要想写一个文件,必须先以可写权限用open系统调用打开一个文件,获得所打开文件的fd,例如 fd=open("/dev/video",O_RDWR)。fd是一个整型值,每新打开一个文件,所获得的fd为当前最大fd加1。Linux系统默认分配了3个文件描述符值:0-standard input,1-standard output,2-standard error。

系统调用通常用于底层文件访问(low-level fileaccess),例如在驱动程序中对设备文件的直接访问。

系统调用是操作系统相关的,因此一般没有跨操作系统的可移植性。

系统调用发生在内核空间,因此如果在用户空间的一般应用程序中使用系统调用来进行文件操作,会有用户空间到内核空间切换的开销。事实上,即使在用户空间使用库函数来对文件进行操作,因为文件总是存在于存储介质上,因此不管是读写操作,都是对硬件(存储器)的操作,都必然会引起系统调用。也就是说,库函数对文件的操作实际上是通过系统调用来实现的。例如C库函数fwrite()就是通过write()系统调用来实现的。

这样的话,使用库函数也有系统调用的开销,为什么不直接使用系统调用呢?

这是因为,读写文件通常是大量的数据(这种大量是相对于底层驱动的系统调用所实现的数据操作单位而言),这时,使用库函数就可以大大减少系统调用的次数。

这一结果又缘于缓冲区技术。在用户空间和内核空间,对文件操作都使用了缓冲区,例如用fwrite 写文件,都是先将内容写到用户空间缓冲区,当用户空间缓冲区满或者写操作结束时,才将用户缓冲区的内容写到内核缓冲区,同样的道理,当内核缓冲区满或写结束时才将内核缓冲区内容写到文件对应的硬件媒介。

库函数调用

标准C库函数提供的文件操作函数如fopen, fread, fwrite, fclose, fflush,fseek等,需包含头文件stdio.h。以fwrite为例,其函数原型为size_tfwrite(const void *buffer, size_t size, size_t item_num, FILE *pf),其操作对象为文件指针FILE *pf,要想写一个文件,必须先以可写权限用fopen函数打开一个文件,获得所打开文件的FILE结构指针pf,例如pf=fopen("~/proj/filename","w")。实际上,由于库函数对文件的操作最终是通过系统调用实现的,因此,每打开一个文件所获得的FILE结构指针都有一个内核空间的文件描述符fd与之对应。同样有相应的预定义的FILE指针:stdin-standard input,stdout-standard output,stderr-standard error。

库函数调用通常用于应用程序中对一般文件的访问。

库函数调用是系统无关的,因此可移植性好。

由于库函数调用是基于C 库的,因此也就不可能用于内核空间的驱动程序中对设备的操作。

函数库调用 VS 系统调用

函数库调用 系统调用
在所有的ANSI C编译器版本中,C库函数是相同的 各个操作系统的系统调用是不同的
它调用函数库中的一段程序(或函数) 它调用系统内核的服务
与用户程序相联系 是操作系统的一个入口点
在用户地址空间执行 在内核地址空间执行
它的运行时间属于“用户时间” 它的运行时间属于“系统”时间
属于过程调用,调用开销较小 需要在用户空间和内核上下文环境间切换,开销较大
在C函数库libc中有大约300个函数 在UNIX中大约有90个系统调用
典型的C函数库调用:system fprintf malloc 典型的系统调用:chdir fork write brk;

系统调用与系统API

1. 系统调用 (可以通过编译内核的方式在内核中添加自己的系统调用)

我们知道,Linux将整个虚拟地址空间划分为两部分:用户空间和内核空间。并且规定,用户空间不能直接访问内核空间,而内核空间则可以访问用户空间。通过这样的级别划分,可以使得内核空间更加的稳定和安全。但是,当用户进程必须访问内核或使用某个内核函数时,就得使用系统调用(System Call)。在Linux中,系统调用是用户空间访问内核空间的唯一途径。

系统调用是内核提供的一组函数接口,它使得用户空间上运行的进程可以和内核之间进行交互。比如,用户进程通过系统调用访问硬件设备或操作系统的某些资源等。系统调用如同内核空间和用户空间的一个传话者。内核如同一个高高在上的帝王,而用户空间的进程则属于级别很小的官员。由于用户进程资质太浅,当它需要得到内核的支持时,它并没有权利直接上报内核,而只能通过系统调用这个传话人来得到内核的支持。

具体的,用户程序通过应用编程接口来使用系统调用,而系统调用则是在内核中通过内核函数来实现的。

2. 应用编程接口(API)(应用程序直接使用API,而不是直接使用系统调用,在API中可能封装了系统调用)

应用编程接口(Application Programming Interface,API)其实就是程序员在用户空间下可以直接使用的函数接口。每个API会对应一定的功能。比如strlen(),它所实现的功能就是求所传递字符串的长度。

有时候,某些API所提供的功能会涉及到与内核空间进行交互。那么,这类API内部会封装系统调用。而不涉及与内核进行交互的API则不会封装系统调用。也就是说,API和系统调用并没有严格对应关系,一个API可能恰好只对应一个系统调用,比如read()系统调用和read();一个API也可能由多个系统调用实现;有时候,一个API的功能可能并不需要内核提供的服务,那么此时这个API也就不需要任何的系统调用,比如abs()。另外,一个系统调用可能还被多个API内部调用。

对于编程者来说,系统调用和API都是一组函数,并无什么两样;但是事实上,系统调用的实现是在内核完成的,API则是在函数库中实现的。

例如:

在内核中实现了write系统调用。在库函数中通过宏定义的方式实现write API。可以在unistd.h中看到write到系统调用的转换。

API是用户程序直接可以使用的函数接口,但如果每个操作系统都拥有只属于自己的API,那么应用程序的移植性将会很差。基于POSIX(Portable Operating System Interface)标准的API拥有很好的可移植性,它定义了一套POSIX兼容标准,这使得按这个标准实现的API可以在各种版本的UNIX中使用。现如今,它也可以在除UNIX之外的操作系统中使用,比如Linux,Windows NT等。

3. 函数库

还记得一个.c文件如何逐步成为可执行文件吗?一个.c文件会经过预处理(宏替换,头文件包含)、编译(将源码转换成汇编代码)、汇编(将汇编代码转换成二进制代码,即.o文件)、链接(将所有的.o文件连接在一起,并加上库文件)四个步骤。

在汇编阶段,输出的是.o文件,即我们常说的目标文件。目标文件并不能直接执行,它需要链接器的再一次加工。

链接器将所有的目标文件集合在一起,加上库文件,最后才能得到可执行文件。

函数库中包含对许多函数的定义,而这些函数的原型声明则散步在不同的头文件中。比如我们常用(也许你并未感知我们频繁的使用这个函数库)的标准函数库libc.so,在其中包含多个我们常用的函数定义,但是这些函数的声明却分布在stdio.h和string.h等头文件中。

其实,我们每次在链接程序时,都必须告诉链接器需要链接到那个库中。只不过通常默认的链接让我们忽视了这一点。

比如,一个简单的helloworld程序中,仅使用了stdio.h头文件。我们当然可以这样轻松的编译:gcc helloworld.c -o helloworld。之所以可以毫无顾忌是因为stdio.h中所声明的函数都定义在libc.so中,而对于这个函数库,连接器是默然链接的。

如果我们编译如下程序:

#include < stdio.h >
#include < math.h >
int main()
{
    double i;
    scanf("%lf",&i);
    printf("%lf",sqrt(i));
    return 0;
}

按照我们以往的编译方法显然是不行的:

$ gcc test.o -o test
test.o: In function `main':
test.c:(.text+0x39): undefined reference to `sqrt'
collect2: ld returned 1 exit status

因为在这个程序中使用了math.h头文件,而这个头文件中声明的函数sqrt()被定义在libm.so函数库中。那么,这个时候应该这样编译:gcc test.c -o test -lm。最后的-lm选项即告诉链接器需要加入libm.so函数库。

上述一步到位的编译方法似乎又无形中掩盖了函数库的加入时间。如果我们按照编译程序的四个步骤依次处理相应文件时,就可以发现只有到了最后的链接过程中才会出现上述错误信息。也就是说,函数库的加入是在链接部分。

从上述内容中,我们知道应用程序直接使用的并不是系统调用而是API。内核中提供的每个系统调用都通过C库封装成相应的API。并且,C库中的函数并不是总和系统调用一一对应,有的C库函数可能内部调用多个系统调用;有的函数内部可能不涉及任何系统调用;也有多个函数都调用一个系统调用的情况。

4. 系统命令

每一个系统命令其实就是一个可执行的程序,这些可执行程序的实现调用了某些系统调用。并且,这些可执行程序又分为普通用户可使用的命令和管理员可使用的命令。根据上述分类,普通用户可用的命令和管理可用的命令分别被存放于/bin和/sbin目录下。

5. 内核函数

内核函数和用户空间中函数并无两样,只不过内核函数是在内核中实现。虽然系统调用是用户进程进入内核的唯一途径,但是系统调用函数内部并不真正实现其功能,而是通过对内核函数的封装。也就是说,用户程序通过某个系统调用进入内核后,会接着去执行这个系统调用对应的内核函数。这个内核函数也称为系统调用的服务例程。

由于内核函数是在内核中实现的,因此它必须符合内核编程的规则,比如函数名以sys_开始,函数定义时候需加asmlinkage标识符等。

posted @ 2025-02-13 23:23  main_c  阅读(12)  评论(0)    收藏  举报  来源