共计 4299 个字符,预计需要花费 11 分钟才能阅读完成。
首先说说零碎调用是什么,当你的代码须要做 IO 操作(open、read、write)、或者是进行内存操作(mmpa、sbrk)、甚至是说要获取一个零碎工夫(gettimeofday),就须要通过零碎调用来和内核进行交互。无论你的用户程序是用什么语言实现的,是 php、c、java 还是 go,只有你是建设在 Linux 内核之上的,你就绕不开零碎调用。
大家能够通过 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 零碎调用进行测试
留神,只能用 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 页表缓存的命中率肯定水平上有所降落。
除了上述堆栈和寄存器等环境的切换外,零碎调用因为特权级别比拟高,也还须要进行一系列的权限校验、有效性等查看相干操作。所以零碎调用的开销绝对函数调用来说要大的多。咱们在计算一下每个零碎调用须要执行的 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 的百分之一,所以还没到了谈零碎调用色变的水平,能理性认识到它的开销既可。
为什么零碎调用之间的耗时相差这么多?因为零碎调用花在内核态用户态的切换上的工夫是差不多的,但区别在于不同的零碎调用当进入到内核态之后要解决的工作不同,呆在内核态里的时候相差较大。
开发内功修炼之 CPU 篇专辑:
- 1. 你认为你的多核 CPU 都是真核吗?多核“假象”
- 2. 据说你只知内存,而不知缓存?CPU 示意很伤心!
- 3.TLB 缓存是个神马鬼,如何查看 TLB miss?
- 4. 过程 / 线程切换到底须要多少开销?
- 5. 协程到底比线程牛在什么中央?
- 6. 软中断会吃掉你多少 CPU?
- 7. 一次零碎调用开销到底有多大?
- 8. 一次简略的 php 申请 redis 会有哪些开销?
- 9. 函数调用太多了会有性能问题吗?
我的公众号是「开发内功修炼」,在这里我不是单纯介绍技术实践,也不只介绍实践经验。而是把实践与实际联合起来,用实际加深对实践的了解、用实践进步你的技术实际能力。欢送你来关注我的公众号,也请分享给你的好友~~~