当前位置: 首页 > Linux

系统调用的开销是多少?

时间:2023-04-06 21:41:38 Linux

首先说一下什么是系统调用。当你的代码需要做IO操作(open,read,write),或者内存操作(mmpa,sbrk),甚至是获取系统时间(gettimeofday)时,都需要通过系统调用与内核进行交互。不管你的用户程序是用什么语言实现的,不管是php、c、java还是go,只要你构建在Linux内核之上,就无法绕过系统调用。您可以使用strace命令查看您的程序正在执行哪些系统调用。比如我查看了生产环境中运行的nginx当前执行的系统调用,如下:#strace-p28927Process28927attachedepoll_wait(6,{{EPOLLIN,{u32=96829456,u64=140312383422480}}},512,-1)=1accept4(8,{sa_family=AF_INET,sin_port=htons(55465),sin_addr=inet_addr("10.143.52.149")},[16],SOCK_NONBLOCK)=13epoll_ctl(6,EPOLL_CTL_ADD,13,{EPOLLIN|EPOLLRDHUP|EPOLLET,{u32=96841984,u64=140312383435008}})=0epoll_wait(6,{{EPOLLIN,{u32=96841984,u64=140312383435008}}},512,60000简单介绍下系统)=call,那么相信各位同学都听过一个建议,系统调用的开销是非常大的,应该尽可能的减少系统调用的次数,以提高自己代码的性能。那么问题来了,能不能给出量化的指标。一次系统调用需要多少开销,需要消耗多少CPU时间?好了,废话不多说,下面直接进行一些测试,用数据说话。实验一首先我对在线服务的nginx做了strace统计,可以看出系统调用的耗时分布在1-15us左右。因此,我们可以大致得出系统调用的耗时在1us级别左右。当然,由于不同的系统调用执行的操作不同,执行时的环境不同,所以不同时间的不同调用之间也会有消耗。时间上下波动。#strace-cp8527strace:Process8527attached%timesecondsusecs/callcallserrorssyscall-----------------------------------------------------------44.440.0007271263epoll_wait27.630.0004521334发送到10.390.00017072521accept45.680.000093812write5.200.000085238recvfrom4.100.000067174writev2.260.00003794close0.310.00000514epoll_ctl实验二下面我们手动写一段代码来测试read系统调用使用fread。所以,fread是一个库函数,在用户态保留了一个缓存,而read意味着你每次调用它,内核都会老老实实地为你执行一次read系统调用。先创建一个固定大小为1M的文件ddif=/dev/zeroof=in.txtbs=1Mcount=1然后编译测试代码#cdtests/test02/#gccmain.c-omain#时间。/mainreal0m0.258suser0m0.030ssys0m0.227s由于上面的实验循环了100万次,每次系统调用的平均耗时约为200ns。系统调用到底在做什么?我们先看一下系统调用消耗的CPU指令数。x86-64CPU有特权级的概念。内核运行在最高层,称为Ring0,用户程序运行在Ring3。一般情况下,用户进程运行在Ring3级别,但磁盘、网卡等外设只能在内核Ring0级别访问。因此,当我们的用户态程序需要访问磁盘等外设时,就需要通过系统调用来切换权限级别。对于普通的函数调用,一般只需要几个寄存器操作。如果有参数或者返回函数,再进行几个用户栈操作。而且用户栈已经被CPUcache捕获了,不需要真正进行内存IO。但是对于系统调用来说,这个过程就稍微麻烦一些。系统调用需要从用户态切换到内核态。由于kernelstack使用的是kernelstack,所以也需要切换stack。SS、ESP、EFLAGS、CS和EIP寄存器都需要切换。而且栈切换后可能会存在一个隐藏的问题,就是CPU调度的指令和数据在一定程度上破坏了原有的局部性,导致在一二三层都有一定的命中率数据缓存和TLB页表缓存。衰退。除了上面的栈和寄存器环境切换,由于系统调用具有比较高的权限级别,还需要进行一系列的权限检查、有效性检查等相关操作。因此,系统调用的开销远大于函数调用。我们正在计算每个系统调用需要执行的CPU指令数。#perfstat./main'./main'的性能计数器统计:251.508810task-clock#0.997CPUsutilized1context-switches#0.000M/sec1CPU-migrations#0.000M/sec97page-faults#0.000M/secsec600,644,444cycles#2.388GHz[83.38%]122,000,095stalled-cycles-frontend#20.31%frontendcyclesidle[83.33%]45,707,976stalled-cycles-backend#7.61%backendcyclesidle[66.66%]1,008,492,1860#sns中每个周期指令#每个insn0.12个停滞周期[83.33%]177,244,889个分支#704.726M/sec[83.32%]7,583个分支未命中#0.00%ofallbranches[83.33%]对实验代码稍作改动,并注释掉read调用在for循环中,然后重新编译并运行#gccmain.c-omain#perfstat./main'./main'的性能计数器统计:3.196978任务时钟#0.893CPU使用0上下文切换#0.000M/sec0CPU迁移#0.000M/sec98页面错误#0.031M/sec7,616,703个周期#2.382GHz[68.92%]5,397,528个停滞周期-前端#70.86%前端周期空闲[68.85%]1,574,438个停滞周期-后端#20.67%后端周期空闲3,359,090条指令#0.44insns1每个周期停滞#1.6每个insn的周期数1,066,900个分支#333.721M/sec799个分支未命中#所有分支的0.07%[80.14%]0.003578966秒时间流逝CPU每个系统调用需要执行的平均指令数(1,008,492,870-3,359,090)/1000000=1005并深入研究系统调用的实现。如果你一定要捡内核的实现,建议你参考《深入理解LINUX内核-第十章系统调用》。最初,系统调用是通过汇编指令int(中断)实现的。当用户态进程发出int$0x80指令时,CPU切换到内核态,开始执行system_call函数。只是后来大家觉得系统调用太慢了,因为int指令要进行一致性和安全检查。后来Intel提供了“快速系统调用”sysenter指令,我们来验证一下。#perfstat-esyscalls:sys_enter_read./mainPerformancecounterstatsfor'./main':1,000,001syscalls:sys_enter_read0.006269041secondstimeelapsed以上实验证明系统调用确实是通过sys_enter指令执行的。相关命令stracestrace-p$PID:实时统计进程中困住的系统调用strace-cp$PID:对一段时间的进程进行汇总,然后以排行榜的形式给出,即非常实用的perfperflist:列出所有可以被perfPoint采样的样本perfstat:统计CPU指令数,上下文切换等默认次数perfstat-eevent:指定采样时间,用于统计perftop:统计函数或整个系统中消耗最多的指令perftop-e:同上,但是可以指定采样结论系统调用虽然使用了“快速系统调用”指令,但是耗时还是在200ns+左右,可能是十多个我们。每个系统调用内核都需要做大量的工作,大约需要执行1000条CPU指令。系统调用的确,开销是相当高的。调用函数的时候是ns级,系统调用直接上升到几百ns,甚至十几us,所以尽量减少系统调用是没错的。但是即使是10us,也依然是1ms的百分之一,所以系统调用的变色还谈不上,能够理性的理解它的开销就足够了。为什么系统调用之间的耗时差异如此之大?因为系统调用从内核态切换到用户态所花费的时间是差不多的,不同的是不同的系统调用进入内核态后要处理的任务不同,停留时差别很大内核模式。练内功专用CPU:1、你以为你的多核CPU是真核生物吗?多核“错觉”2、听说你只知道内存,不知道缓存?CPU表示很伤心!3.TLB缓存是个鬼,如何查看TLBmiss?4、进程/线程切换需要多少开销?5、协程比线程好在哪里?6.softirq会吃掉多少CPU?7、一次系统调用的开销是多少?8、一个简单的php请求redis的开销是多少?9、函数调用过多会不会有性能问题?我的公众号是《练内功》。在这里,我不是简单地介绍技术理论,也不是只介绍实践经验。而是理论联系实际,用实践加深对理论的理解,用理论提高技术实践能力。欢迎关注我的公众号,分享给你的朋友吧~~~