一次系统调用开销到底有多大?

本文深入探讨了系统调用的原理与性能开销,通过实际测试量化了系统调用的时间成本,揭示了其在不同场景下的耗时差异,并解析了系统调用的内核实现细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先说说系统调用是什么,当你的代码需要做IO操作(open、read、write)、或者是进行内存操作(mmpa、sbrk)、甚至是说要获取一个系统时间(gettimeofday),就需要通过系统调用来和内核进行交互。无论你的用户程序是用什么语言实现的,是php、c、java还是go,只要你是建立在Linux内核之上的,你就绕不开系统调用。

图1.png

大家可以通过strace命令来查看到你的程序正在执行哪些系统调用。比如我查看了一个正在生产环境上运行的nginx当前所执行的系统调用,如下:

# strace -p 28927
Process 28927 attached  
epoll_wait(6, {{EPOLLIN, {u32=96829456, u64=140312383422480}}}, 512, -1) = 1  
accept4(8, {sa_family=AF_INET, sin_port=htons(55465), sin_addr=inet_addr("10.143.52.149")}, [16], SOCK_NONBLOCK) = 13  
epoll_ctl(6, EPOLL_CTL_ADD, 13, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=96841984, u64=140312383435008}}) = 0  
epoll_wait(6, {{EPOLLIN, {u32=96841984, u64=140312383435008}}}, 512, 60000) = 1  
 

简单介绍了下系统调用,那么相信各位同学都听说过一个建议,就是系统调用的开销很大,要尽量减少系统调用的次数,以提高你的代码的性能。那么问题来了,我们是否可以给出量化的指标。一次系统调用到底要多大的开销,需要消耗掉多少CPU时间?好了,废话不多说,我们直接进行一些测试,用数据来说话。

实验1

首先我对线上正在服务的nginx进行strace统计,可以看出系统调用的耗时大约分布在1-15us左右。因此我们可以大致得出结论,系统调用的耗时大约是1us级别的,当然由于不同系统调用执行的操作不一样,执行当时的环境不一样,因此不同的时刻,不同的调用之间会存在耗时上的上下波动。

# strace -cp 8527  
strace: Process 8527 attached  
% time     seconds  usecs/call     calls    errors syscall  
------ ----------- ----------- --------- --------- ----------------  
 44.44    0.000727          12        63           epoll_wait  
 27.63    0.000452          13        34           sendto 
 10.39    0.000170           7        25        21 accept4  
  5.68    0.000093           8        12           write  
  5.20    0.000085           2        38           recvfrom  
  4.10    0.000067          17         4           writev  
  2.26    0.000037           9         4           close  
  0.31    0.000005           1         4           epoll_ctl 

实验2

我们再手工写段代码,对read系统调用进行测试,代码参见test02

注意,只能用read库函数来进行测试,不要使用fread。因此fread是库函数在用户态保留了缓存的,而read是你每调用一次,内核就老老实实帮你执行一次read系统调用。

首先创建一个固定大小为1M的文件

dd if=/dev/zero of=in.txt bs=1M count=1

然后再编译代码进行测试

#cd tests/test02/  
#gcc main.c -o main  
#time ./main  
real    0m0.258s   
user    0m0.030s  
sys     0m0.227s  

由于上述实验是循环了100万次,所以平均每次系统调用耗时大约是200ns多一些。

系统调用到底在干什么?

先看看系统调用话费的CPU指令数

x86-64 CPU有一个特权级别的概念。内核运行在最高级别,称为Ring0,用户程序运行在Ring3。正常情况下,用户进程都是运行在Ring3级别的,但是磁盘、网卡等外设只能在内核Ring0级别下来来访问。因此当我们用户态程序需要访问磁盘等外设的时候,要通过系统调用进行这种特权级别的切换

对于普通的函数调用来说,一般只需要进行几次寄存器操作,如果有参数或返回函数的话,再进行几次用户栈操作而已。而且用户栈早已经被CPU cache接住,也并不需要真正进行内存IO。

但是对于系统调用来说,这个过程就要麻烦一些了。系统调用时需要从用户态切换到内核态。由于内核态的栈用的是内核栈,因此还需要进行栈的切换。SS、ESP、EFLAGS、CS和EIP寄存器全部都需要进行切换。

而且栈切换后还可能有一个隐性的问题,那就是CPU调度的指令和数据一定程度上破坏了局部性原来,导致一二三级数据缓存、TLB页表缓存的命中率一定程度上有所下降。

除了上述堆栈和寄存器等环境的切换外,系统调用由于特权级别比较高,也还需要进行一系列的权限校验、有效性等检查相关操作。所以系统调用的开销相对函数调用来说要大的多。我们在test02的基础上计算一下每个系统调用需要执行的CPU指令数。

# perf stat ./main

 Performance counter stats for './main':

        251.508810 task-clock                #    0.997 CPUs utilized
                 1 context-switches          #    0.000 M/sec
                 1 CPU-migrations            #    0.000 M/sec
                97 page-faults               #    0.000 M/sec
       600,644,444 cycles                    #    2.388 GHz                     [83.38%]
       122,000,095 stalled-cycles-frontend   #   20.31% frontend cycles idle    [83.33%]
        45,707,976 stalled-cycles-backend    #    7.61% backend  cycles idle    [66.66%]
     1,008,492,870 instructions              #    1.68  insns per cycle
                                             #    0.12  stalled cycles per insn [83.33%]
       177,244,889 branches                  #  704.726 M/sec                   [83.32%]
             7,583 branch-misses             #    0.00% of all branches         [83.33%]

对实验代码进行稍许改动,把for循环中的read调用注释掉,再重新编译运行

# gcc main.c -o main  
# perf stat ./main  

 Performance counter stats for './main':  

          3.196978 task-clock                #    0.893 CPUs utilized
                 0 context-switches          #    0.000 M/sec
                 0 CPU-migrations            #    0.000 M/sec
                98 page-faults               #    0.031 M/sec
         7,616,703 cycles                    #    2.382 GHz                       [68.92%]
         5,397,528 stalled-cycles-frontend   #   70.86% frontend cycles idle      [68.85%]  
         1,574,438 stalled-cycles-backend    #   20.67% backend  cycles idle  
         3,359,090 instructions              #    0.44  insns per cycle  
                                             #    1.61  stalled cycles per insn  
         1,066,900 branches                  #  333.721 M/sec
               799 branch-misses             #    0.07% of all branches           [80.14%]  

       0.003578966 seconds time elapsed  

平均每次系统调用CPU需要执行的指令数(1,008,492,870 - 3,359,090)/1000000 = 1005。

再深挖系统调用的实现

如果非要扒到内核的实现上,我建议大家参考一下《深入理解LINUX内核-第十章系统调用》。最初系统调用是通过汇编指令int(中断)来实现的,当用户态进程发出int $0x80指令时,CPU切换到内核态并开始执行system_call函数。 只不过后来大家觉得系统调用实在是太慢了,因为int指令要执行一致性和安全性检查。后来Intel又提供了“快速系统调用”的sysenter指令,我们验证一下。

# perf stat -e syscalls:sys_enter_read ./main  

 Performance counter stats for './main':  

            1,000,001 syscalls:sys_enter_read  

       0.006269041 seconds time elapsed  

上述实验证明,系统调用确实是通过sys_enter指令来进行的。

相关命令

  • strace
    • strace -p $PID: 实时统计进程陷入的系统调用
    • strace -cp $PID: 对进程执行一段时间内的汇总,然后以排行榜的形式给出来,非常实用
  • perf
    • perf list: 列出所有能够perf采样点
    • perf stat: 统计CPU指令数、上下文切换等缺省时间
    • perf stat -e 事件: 指定采样时间进行统计
    • perf top: 统计整个系统内消耗最多的函数或指令
    • perf top -e: 同上,但是可以指定采样点

结论

  • 系统调用虽然使用了“快速系统调用”指令,但耗时仍大约在200ns+,多的可能到十几us
  • 每个系统调用内核要进行许多工作,大约需要执行1000条左右的CPU指令

系统调用确实开销蛮大的,函数调用时ns级别的,系统调用直接上升到了百ns,甚至是十几us,所以确实应该尽量减少系统调用。但是即使是10us,仍然是1ms的百分之一,所以还没到了谈系统调用色变的程度,能理性认识到它的开销既可。

为什么系统调用之间的耗时相差这么多?因为系统调用花在内核态用户态的切换上的时间是差不多的,但区别在于不同的系统调用当进入到内核态之后要处理的工作不同,呆在内核态里的时候相差较大。

欢迎关注个人公众号“开发内功修炼”,打通理论与实践的任督二脉。

参考资料

<think>好的,我现在需要回答用户关于系统调用和普通用户函数调用区别的问题。用户已经提供了四个引用资料,我需要结合这些资料以及之前的对话内容来构建一个全面且准确的回答。 首先,系统调用和函数调用的基本区别是什么?根据引用[1],系统调用操作系统核心提供,运行在核心态,而普通函数调用由函数库或用户自己提供,运行在用户态。这一点是核心区别,需要在回答中突出。 接下来,引用[3]详细列出了两者的不同点,包括执行环境(用户态vs内核态)、功能(访问硬件资源vs程序内部功能)、开销vs小)、执行时间分类(系统时间vs用户时间)等。这些都需要分点说明,确保覆盖用户提到的执行环境、开销、功能等方面。 然后,引用[2]和[4]提到了函数调用的开销主要来自调用过程、参数传递等,而系统调用需要切换到内核上下文,开销。这部分需要详细解释,特别是上下文切换带来的性能影响。 用户之前的问题是关于进程同步互斥的特征,可能对系统调用在同步互斥中的应用有所了解,比如信号量和互斥锁的实现可能涉及系统调用。不过当前问题更侧重于两者的区别,所以可能需要简要提及系统调用在实现同步机制中的作用,但重点还是放在区别上。 需要注意的是,用户提供的引用中有重复内容,比如引用[2]和[3]都讨论了区别,需要整合这些信息,避免冗余。同时,确保引用正确的来源,如引用[1]提到系统调用运行于核心态,引用[3]则详细说明两者的执行环境和开销差异。 另外,用户可能希望得到结构清晰的回答,所以分点列出各个方面的差异,如执行环境、功能、开销、实现方式等,每个点下用简明的语言解释,并引用相关资料支持。例如,在开销部分,可以引用[3]和[4]说明系统调用的上下文切换开销,而函数调用主要是栈操作和返回地址保存。 最后,生成的相关问题需要与主题紧密相关,帮助用户进一步探索。比如,系统调用的具体流程、优化方法,或者实际应用案例等。 现在需要组织这些信息,确保回答符合用户的要求:结构清晰,分点说明,引用正确,语言准确,并生成相关问题。</think>--- ### 系统调用与普通用户函数调用的区别 #### 1. **执行环境与权限级别** - **系统调用**: - 运行于**内核态**(核心态),由操作系统内核直接提供,例如文件读写、进程创建等操作。 - 需要从用户态切换到内核态,通过软中断(如`int 0x80`)或专用指令(如`syscall`)触发[^1]。 - **示例**:Linux中`open()`函数需要内核权限访问硬件设备[^1]。 - **普通函数调用**: - 运行于**用户态**,由函数库(如C标准库)或用户代码实现。 - 直接在用户空间执行,无需切换权限模式[^3]。 - **示例**:调用`printf()`时,用户程序仅操作内存缓冲区,最终通过系统调用`write()`输出到终端[^3]。 --- #### 2. **功能与资源访问范围** - **系统调用**: - **功能**:访问硬件资源(如磁盘、网络)、管理进程/线程、实现同步互斥(如信号量`sem_wait()`)。 - **权限**:可绕过用户程序直接操作内核数据结构(如进程控制块PCB)[^1]。 - **普通函数调用**: - **功能**:处理程序内部逻辑(如数学计算、字符串处理),无法直接访问硬件或内核资源。 - **权限**:仅限用户空间内存和库函数提供的功能[^3]。 --- #### 3. **执行开销** - **系统调用**: - **高开销**:需切换CPU模式(用户态→内核态→用户态),保存/恢复寄存器、堆栈等上下文,耗时约为数百至数千时钟周期[^3]。 - **示例**:Linux中一次`read()`系统调用可能涉及缓冲区复制和中断处理[^1]。 - **普通函数调用**: - **低开销**:仅需保存返回地址和局部变量到栈中,无上下文切换,耗时通常为几到几十时钟周期[^4]。 - **优化**:内联函数(`inline`)可进一步减少调用开销。 --- #### 4. **实现方式与依赖关系** - **系统调用**: - 通过**内核API**暴露给用户,例如Linux的`glibc`封装了系统调用接口(如`fork()`对应`sys_fork`内核函数)。 - 依赖操作系统版本和架构(如x86与ARM的系统调用号不同)。 - **普通函数调用**: - 由编译器直接生成跳转指令(如`call`),依赖函数库(如`libm`中的数学函数)[^4]。 - 可跨平台编译执行(若库函数兼容)[^3]。 --- ### 对比总结表 | **维度** | **系统调用** | **普通函数调用** | |-------------------|---------------------------------------|---------------------------------| | **执行环境** | 内核态 | 用户态 | | **权限** | 访问硬件/内核资源 | 仅限用户空间资源 | | **开销** | 高(上下文切换) | 低(栈操作) | | **实现依赖** | 操作系统内核 | 函数库/用户代码 | | **典型示例** | `open()`, `fork()`, `sem_post()` | `sqrt()`, `strlen()`,自定义函数 | --- ### 实际场景示例 - **系统调用链**: 用户调用`printf()`→C库封装`write()`系统调用→内核处理数据写入终端[^3]。 - **性能影响**: 频繁的系统调用(如循环中调用`gettimeofday()`)会显著降低程序性能,可通过批量处理优化[^1]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值