首页
关于
Search
1
Lua使用调试库hook函数调用
705 阅读
2
傻瓜式快速搭建l2tp
634 阅读
3
游戏邮件系统数据设计因素
625 阅读
4
傻瓜式安装chatgpt-web工具
579 阅读
5
Linux内核数据结构kfifo小结(TODO)
574 阅读
项目技术
项目思考
开发环境
数据库
编程语言
生活与阅读
哲学
登录
Search
标签搜索
nodejs
npm
资深IT牛马
累计撰写
57
篇文章
累计收到
0
条评论
首页
栏目
项目技术
项目思考
开发环境
数据库
编程语言
生活与阅读
哲学
页面
关于
搜索到
57
篇与
的结果
2021-06-26
为什么系统调用会消耗较多资源【转载】
这篇文章非常直白科普说明了系统调用过程,主要可以解决系统调用背后疑惑,通过经典的Hello World开始,讲述软件中断、SYSCALL/SYSENTER 等汇编指令、vDSO三方方式实现系统调用及其性能差异。非常推荐阅读。转自:为什么系统调用会消耗较多资源另外推荐相关:Linux 系统调用权威指南英文版本地址:https://blog.packagecloud.io/eng/2016/04/05/the-definitive-guide-to-linux-system-calls/系统调用是计算机程序在执行的过程中向操作系统内核申请服务的方法,这可能包含硬件相关的服务、新进程的创建和执行以及进程调度,对操作系统稍微有一些了解的人都知道 — 系统调用为用户程序提供了操作系统的接口[1]图 1 - 操作系统接口C 语言的著名的 glibc 封装了操作系统提供的系统调用并提供了定义良好的接口[2],工程师可以直接使用器中封装好的函数开发上层的应用程序,其他编程语言的标准库也会封装系统调用,它们对外提供语言原生的接口,内部使用汇编语言触发系统调用。我们在使用标准库时需要经常与系统调用打交道,只是很多时候我们不知道标准库背后的实现,以常见的 Hello World 程序为例,这么简单的几行函数在真正运行时会执行几十次系统调用:#include <stdio.h> int main() { printf("Hello, World!"); return 0; } $ gcc hello.c -o hello $ strace ./hello execve("./hello", ["./hello"], 0x7ffd64dd8090 /* 23 vars */) = 0 brk(NULL) = 0x557b449db000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=26133, ...}) = 0 mmap(NULL, 26133, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f645455a000 close(3) = 0 ... munmap(0x7f645455a000, 26133) = 0 fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 0), ...}) = 0 brk(NULL) = 0x557b449db000 brk(0x557b449fc000) = 0x557b449fc000 write(1, "Hello, World!", 13Hello, World!) = 13 exit_group(0) = ? +++ exited with 0 +++strace 是 Linux 中用于监控和篡改进程与内核之间操作的工具,上述命令会打印出 hello 执行过程中触发系统调用、参数以及返回值等信息。执行 Hello World 程序时触发的多数系统调用都是程序启动触发的,只有 munmap 后的系统调用才是 printf 函数触发的,作为应用程序我们能做的事情非常有限,很多功能都需要依赖操作系统提供的服务。多数编程语言的函数调用只需要分配新的栈空间、向寄存器写入参数并执行 CALL 汇编指令跳转到目标地址执行函数,在函数返回时通过栈或者寄存器返回参数[3]。与函数调用相比,系统调用会消耗更多的资源,如下图所示,使用 SYSCALL 指定执行系统调用消耗的时间是 C 函数调用的几十倍[4]:图 2 - 系统调用与函数调用耗时比较上图中的 vDSO 全称是虚拟动态链接对象(Virtual Dynamically Shared Object、vDSO),它可以减少系统调用的消耗的时间,我们会在后面详细分析它的实现原理。getpid(2) 是一个相对比较快的系统调用,该系统调用不包含任何参数,只会切换到内核态、读取变量并返回 PID,我们可以将它的执行时间当做系统调用的基准测试;除了 getpid(2) 之外,使用 close(999) 系统调用关闭不存在的文件描述符会消耗更少的资源5,与 getpid(2) 相比大概会少 20 个 CPU 周期[6],当然想要实现用于测试额外开销的系统调用,使用自定义的空函数应该是最完美的选择,感兴趣的读者可以自行尝试一下。图 3 - 系统调用的三种方法从上面的系统调用与函数调用的基准测试中,我们可以发现不使用 vSDO 加速的系统调用需要的时间是普通函数调用的几十倍,为什么系统调用会带来这么大的额外开销,它在内部到底执行了哪些工作呢,本文将介绍 Linux 执行系统调用的三种方法:使用软件中断(Software interrupt)触发系统调用;使用 SYSCALL / SYSENTER 等汇编指令触发系统调用;使用虚拟动态共享对象(virtual dynamic shared object、vDSO)执行系统调用;软件中断中断是向处理器发送的输入信号,它能够表示某个时间需要操作系统立刻处理,如果操作系统接收了中断,那么处理器会暂停当前的任务、存储上下文状态、并执行中断处理器处理发生的事件,在中断处理器结束后,当前处理器会恢复上下文继续完成之前的工作[7]。图 4 - 硬件中断和软件中断根据事件发出者的不同,我们可以将中断分成硬件和软件中断两种,硬件中断是由处理器外部的设备触发的电子信号;而软件中断是由处理器在执行特定指令时触发的,某些特殊的指令也可以故意触发软件中断[8]。在 32 位的 x86 的系统上,我们可以使用 INT 指令来触发软件中断,早期的 Linux 会使用 INT 0x80 触发软件中断、注册特定的中断处理器 entry_INT80_32 来处理系统调用,我们来了解一下使用软件中断执行系统调用的具体过程[9]:1.应用程序通过调用 C 语言库中的函数发起系统调用;2.C 语言函数通过栈收到调用方传入的参数并将系统调用需要的参数拷贝到寄存器;3.Linux 中的每一个系统调用都有特定的序号,函数会将系统调用的编号拷贝到 eax 寄存器;4.函数执行 INT 0x80 指令,处理器会从用户态切换到内核态并执行预先定义好的处理器;5.执行中断处理器 entry_INT80_32 处理系统调用;执行 SAVE_ALL 将寄存器的值存储到内核栈上并调用 do_int80_syscall_32;调用 do_syscall_32_irqs_on 检查系统调用的序号是否合法;在系统调用表 ia32_sys_call_table 中查找对应的系统调用实现并传入寄存器的值;系统调用在执行期间会检查参数的合法性、在用户态内存和内核态内存之间传输数据,系统调用的结果会被存储到 eax 寄存器中;从内核栈中恢复寄存器的值并将返回值放到栈上;系统调用会返回 C 函数,包装函数会将结果返回给应用程序;6.如果系统调用服务在执行过程中出现了错误,C 语言函数会将错误存储在全局变量 errno 中并根据系统调用的结果返回一个用整数 int 表示的状态;图 5 - 系统调用的执行步骤从上述系统调用的执行过程中,我们可以看到基于软件中断的系统调用是一个比较复杂的流程,应用程序通过软件中断陷入内核态并在内核态查询并执行系统调用表注册的函数,整个过程不仅需要存储寄存器中的数据、从用户态切换至内核态,还需要完成验证参数的合法性,与函数调用的过程相比确实会带来很多的额外开销[10]。实际上,使用 INT 0x80 来触发系统调用早就是过去时了,大多数的程序都会尽量避免这种触发方式。然而这一规则也不是通用的,因为 Go 语言团队在做基准测试时发现 INT 0x80 触发系统调用在部分操作系统上与其他方式有着几乎相同的性能11,所以在 Android/386 和 Linux/386 等架构上仍然会使用中断来执行系统调用[12]汇编指令因为使用软件中断实现的系统调用在 Pentium 4 的处理器上表现非常差13。Linux 为了解决这个问题,在较新的版本使用了新的汇编指令 SYSENTER / SYSCALL,它们是 Intel 和 AMD 上用于实现快速系统调用的指令,我们会在 32 位的操作系统上使用 SYSENTER / SYSEXIT,在 64 位的操作系统上使用 SYSCALL / SYSRET:图 6 - 快速系统调用指令上述的几个汇编指令是低延迟的系统调用和返回指令,它们会认为操作系统实现了线性内存模型(Linear-memory Model),极大地简化了操作系统系统调用和返回的过程,其中包括不必要的检查、预加载参数等,与软件中断驱动的系统调用相比,使用快速系统调用指令可以减少 25% 的时钟周期[13]。线性内存模型是一种内存寻址的常见范式,在这种模式中,线性内存与应用程序存储在单一连续的空间地址中,CPU 可以不借助内存碎片或者分页技术使用地址直接访问可用的内存地址。在 64 位的操作系统上,我们会使用 SYSCALL / SYSRET 进入和退出系统调用,该指令会在操作系统最高权限等级中执行。内核在初始化时会调用 syscall_init 函数将 entry_SYSCALL_64 存入 MSR 寄存器(Model Specific Register、MSR)中,MSR 寄存器是 x86 指令集中用于调试、追踪以及性能监控的控制寄存器14:void syscall_init(void) { wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS); wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64); ... }当内核收到了用户程序触发的系统调用时,它会在 MSR 寄存器中读取需要执行的函数并按照 x86-64 的调用惯例在寄存器中读取系统调用的编号以及参数,你能在 entry_SYSCALL_64 函数的注释中找到相关的调用惯例。汇编函数 entry_SYSCALL_64 会在执行的过程中调用 do_syscall_64,它的实现与上一节中的 do_int80_syscall_32 有些相似,它们都会在系统调用表中查找函数并传入寄存器中的参数。与 INT 0x80 通过触发软件中断实现系统调用不同,SYSENTER 和 SYSCALL 是专门为系统调用设计的汇编指令,它们不需要在中断描述表(Interrupt Descriptor Table、IDT)中查找系统调用对应的执行过程,也不需要保存堆栈和返回地址等信息,所以能够减少所需要的额外开销。vDSO虚拟动态共享对象(virtual dynamic shared object、vDSO)是 Linux 内核对用户空间暴露内核空间部分函数的一种机制15,简单来说,我们将 Linux 内核中不涉及安全的系统调用直接映射到用户空间,这样用户空间中的应用程序在调用这些函数时就不需要切换到内核态以减少性能上的损失。vDSO 使用了标准的链接和加载技术,作为一个动态链接库,它由 Linux 内核提供并映射到每一个正在执行的进程中,我们可以使用如下所示的命令查看该动态链接库在进程中的位置:$ ldd /bin/cat linux-vdso.so.1 (0x00007fff2709c000) ... $ cat /proc/self/maps ... 7f28953ce000-7f28953cf000 r--p 00027000 fc:01 2079 /lib/x86_64-linux-gnu/ld-2.27.so 7f28953cf000-7f28953d0000 rw-p 00028000 fc:01 2079 /lib/x86_64-linux-gnu/ld-2.27.so 7f28953d0000-7f28953d1000 rw-p 00000000 00:00 0 7ffe8ca4d000-7ffe8ca6e000 rw-p 00000000 00:00 0 [stack] 7ffe8ca8d000-7ffe8ca90000 r--p 00000000 00:00 0 [vvar] 7ffe8ca90000-7ffe8ca92000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]因为 vDSO 是由操作系统直接提供的,所以它并不存在对应的文件,在程序执行的过程中我们也能在虚拟内存中看到它加载的位置。vDSO 可以为用户程序提供虚拟的系统调用,它会使用内核提供的数据在用户态模拟系统调用:图 7 - 内核和用户控件的初始化系统调用 gettimeofday 是一个非常好的例子,如上图所示,使用 vDSO 的系统调用 gettimeofday 会按照如下所示的步骤进行初始化16:1.内核中的 ELF 加载器会负责映射 vDSO 的内存页并设置辅助向量(Auxiliary Vector)中 AT_SYSINFO_EHDR,该标签存储了 vDSO 的基地址;2.动态链接器会查询辅助向量中 AT_SYSINFO_EHDR,如果设置了该标签会链接 vDSO;3.libc 在初始化时会在 vDSO 中查找 __vdso_gettimeofday 符号并将符号链接到全局的函数指针上;除了 gettimeofday 之外,多数架构上的 vDSO 还包含 clock_gettime、clock_getres 和 rt_sigreturn 等三个系统调用,这些系统调用完成功能相对来说比较简单,也不会带来安全上的问题,所以将它们映射到用户空间可以明显地提高系统调用的性能,就像我们在图二中看到的,使用 vDSO 可以将上述几个系统调用的时间提高几十倍。总结当我们在编写应用程序时,系统调用并不是一个离我们很远的概念,一个简单的 Hello World 会在执行时触发几十次系统调用,而在线上出现性能问题时,可能也需要我们与系统调用打交道。虽然程序中的系统调用非常频繁,但是与普通的函数调用相比,它会带来明显地额外开销:使用软件中断触发的系统调用需要保存堆栈和返回地址等信息,还要在中断描述表中查找系统调用的响应函数,虽然多数的操作系统不会使用 INT 0x80 触发系统调用,但是在一些特殊场景下,我们仍然需要利用这一古老的技术;使用汇编指令 SYSCALL / SYSENTER 执行系统调用是今天最常见的方法,作为专门为系统调用打造的指令,它们可以省去一些不必要的步骤,降低系统调用的开销;使用 vSDO 执行系统调用是操作系统为我们提供的最快路径,该方式可以将系统调用的开销与函数调用拉平,不过因为将内核态的系统调用映射到『用户态』确实存在安全风险,所以操作系统也仅会放开有限的系统调用;应用程序能够完成的工作相当有限,我们需要使用操作系统提供的服务才能编写功能丰富的用户程序。系统调用作为操作系统提供的接口,它与底层的硬件关系十分紧密,因为硬件的种类繁杂,所以不同架构要使用不同的指令,随着内核的快速演进,想要找到准确的资料也非常困难,不过了解不同系统调用的实现原理对我们认识操作系统也有很大的帮助。到最后,我们还是来看一些比较开放的相关问题,有兴趣的读者可以仔细思考一下下面的问题:vDSO 提供的系统调用 rt_sigreturn 有哪些作用?vDSO 提供的四种系统调用中三种都与获取时间有关,为什么它可以在用户态提供 rt_sigreturn,不存在安全风险么?Fastest Linux system call https://stackoverflow.com/a/48913894Wikipedia: Interrupt https://en.wikipedia.org/wiki/InterruptHardware & Software interrupts https://en.wikipedia.org/wiki/Interrupt#Hardware_interruptsMichael Kerrisk. 2010. The Linux Programming Interface: A Linux and UNIX System Programming Handbook (1st. ed.). Chapter 3. P44. No Starch Press, USA.“int 0x80” system call path performance implications. P82. https://francescoquaglia.github.io/TEACHING/PMMC/SLIDES/kernel-programming-basics.pdfruntime, syscall: use int $0x80 to invoke syscalls on android/386 https://go-review.googlesource.com/c/go/+/16996/runtime, syscall: switch linux/386 to use int 0x80 https://go-review.googlesource.com/c/go/+/19833/Intel P6 vs P7 system call performance https://lkml.org/lkml/2002/12/9/13Wikipedia: Model-specific register https://en.wikipedia.org/wiki/Model-specific_registerWikipedia: vDSO https://en.wikipedia.org/wiki/VDSOKernel and userspace setup https://vvl.me/pdf/LPC_vDSO.pdf
2021年06月26日
13 阅读
0 评论
0 点赞
2021-06-26
跳表,一种简单的搜索数据结构
跳表是用来快速搜索的数据结构,在大部分情况下可以和平衡树效率相近。他是基于有序双向链表的查找。可以认为是一种兼顾了有序数组快速搜索和链表的随意高效插入两种特性的数据结构。特别是在实现排行榜业务时,基本就是使用他了。而且广为人知的Redis的zset也是跳表的实现。数据结构概览可以看到每个节点有backward指针指向后置节点。Redis中排序是从小到大的。也就是指向比当前节点小的下一个节点。当前节点指向前进方向(变大)节点的指针是有多个的。这也是实现跳进访问,提高有效搜索的关键。在Redis中还有span成员也重要,用来计算搜索到当前节点经过的节点数量,对应的也就是业务中的排名。如果仅用来搜索是不用维护span这个数值的。Redis实现概览出自Redis设计与实现实现关键链表的Header其实是一个伪节点,他从初始化就包含了最大的层数。在Redis下是32层。其他节点的层数是动态的根据幂次定律生成的(掷色子)。查找是从左边顶层往下搜索,遇到小的就进行一次跳跃。否则继续往下层找。直到找到或者到了最小层也无法找到。
2021年06月26日
19 阅读
0 评论
0 点赞
Linux信号笔记
信号是什么 信号是一种软中断机制实现的进程异步通讯方式。信号常见几种产生方式:用户终端某些按键,比如CTRL+C,CTRL+D,CTRL+Z,CTRL+S,CTRL+Q硬件异常。除0,无效的内存引用等用户通过kill、tkill、tgkill、sigqueue、raise函数发送给另一个进程或者进程组或者自己进程一个信号通过kill命令发送信号当检测到某种软件条件已经发生,并应将其通知有关进程。如SIGALRM、SIGPIPE(对方已经关闭读,但本端想写)、SIGURG(有带外数据)什么是可靠信号,什么是不可靠信号 由于历史原因,小于32的信号是不可靠信号。反之则为可靠信号。不可靠信号有丢失的可能信号生命周期和进程处理信号的时机 在进程安装信号处理函数后,内核向进程发送信号,其实是向目标进程相关的bitmap结构(信号向量表)中加入记录,此时信号处于pending状态。对于小于32编号的不可靠信号,如果当前已经有这个信号处理pending中,则丢弃。对于可靠信号则进入一个pending队列中。当进程调用系统函数进入内核态返回用户态之前内核进行信号的投递(delivery),转而去执行信号处理函数。这就是一个信号的完整生命周期。如果设定为阻塞,则会一直阻塞。程序对信号的响应分类执行系统默认动作。如果是可靠信号,受到屏蔽字阻塞设定的影响。这些行为一般可以归类为:ignore、terminate、core、stop、continue设置为忽略此信号。SIGKILL和SIGTOP是不能被忽略的,其他信号都可以忽略。如果是可靠信号,并设置为忽略,则不受到屏蔽字阻塞设定的影响,直接丢弃。不可靠信号没有阻塞的概念。设置为指定的用户信号处理函数处理。比如捕捉到SIGCHLD信号则表示一个子进程已经终止,可以无阻塞的调用waitpid来取得子进程ID和终止状态。同理SIGKILL和SIGTOP也不能被用户捕捉。如果是可靠信号,受到屏蔽字阻塞设定的影响。进程有多线程时,信号是由哪个线程处理的,是什么时机处理的。多个线程来说,信号处理函数是共享的。每个线程都拥有独立的阻塞信号掩码。对于多线程的进程,kill和sigqueue发送的信号必须面对所有的线程,而不是某个线程。而系统调用tkill和tgkill发送的信号,又必须递送给进程下某个特定的线程。每个线程都有自己的私有挂起队列(pending),但是进程里的所有线程都会共享一个公有的挂起队列(shared_pending)。如果是发送了指定线程,则进行线程自己队列。那么多线程情况下发送给进程的信号,到底由哪个线程来负责处理?优先查询进程的主线程是否方便处理信号。如果主线程不方便,则会遍历线程组中的其他线程。如果某个线程A设定了对这个信号阻塞、或者正在退出则不由A线程处理。最后的效果就是在这些可以处理的线程中随机一个。信号处理函数有什么要注意的,malloc是否可用呢?根据上述信号处理函数的执行流程可以看出一位问题是,普通函数可能被中断,而后执行信号函数。这是就有异步安全问题。或者说重入问题。有重入问题的函数都是不用用于信号处理函数中。这和多线程重入的概念相似,但描述的事情不同的。线程安全的函数不一定是异步重入安全的。一般的不可重入函数(man 7 signal):使用了静态变量,典型的是strtok、localtime等函数使用了malloc或free函数标准I/O函数,如printf即使是可重入函数,在信号处理函数调用前要先备份errno变量,在离开前恢复errno变量。信号为什么会打断系统调用?如何处理? 系统调用在执行期间,很可能会收到信号,此时进程可能不得不从系统调用中返回,去执行信号处理函数。对于执行时间比较久的系统调用(如wait、read等)被信号中断的可能性会大大增加。系统调用被中断后,一般会返回失败,并置错误码为EINTR。 Linux操作系统提供了一个标志位SA_RESTART来告诉内核,被信号中断后是否要重启系统调用。如果该标志位为1,则表示如果系统调用被信号中断,那么内核会自动重启系统调用。有些信号即使设置了SA_RESTART也不会自动重启,常见的有epoll_wait, select, 和有超时设置的accept和有超时设置的recv. 这些不能自动重启的函数可以man 7 signal中查看。
2021年06月26日
13 阅读
0 评论
0 点赞
哈希冲突TODO
设计哈希表时,选择不同的哈希函数有不同性能表现。重点在于哪种散列函数可以有效避免冲突。如果出现了冲突,一般都有哪些冲突解决办法。线性探测二次探测开链法(GCC使用的SGI版本STL就是基于此方法) 虽然开链法并不要求表格大小必须为质数,但SGI STL 仍然以质数来设计表格大小,并且将28个质数(逐渐呈现大约两倍的关系)计算好,以备随时访问,同时提供一个函数,用来查询在这28个质数之中,“最接近某数并大于某数”的质数。在JDK8以前也是使用链表来处理冲突。JDK8以后在链表超过8时使用平衡树来存储。建议使用头插法而不是尾插法入链。因为最近增加的元素有很大概率被下次访问。常见的哈希算法:MD5SUM、SHA1、DJBX33A(Java HashCode)、CRC32(部分硬件原语加速)、CRC16、MurMurHash(速度比MD5快)、SipHash(Redis\Python使用)哈希算法从安全性又可以分两类加密哈希函数如SHA非加密哈希函数。服务器中使用哈希算法需要考虑:哈希攻击(hash flooding attack)。不要对不明数据进行哈希。而MurMurHash和SipHash使用了种子的概念实现随机化哈希,增加了攻击的难度。因此SipHash也被称为带密钥哈希算法(Keyed Hash Function),天生免疫哈希攻击。加盐哈希方案。通过增加一些随机的盐参数,把需要加密的账号密码更安全的存储。
2021年06月26日
56 阅读
0 评论
0 点赞
Groovy语法
Groovy运行于Java虚拟机上的,所以语法也自然支持Java语法。他也有对Java语法的扩展。注意Java是静态强类型语言, 而Groovy是一个动态强类型语言。前面也已经说了他是运行于Java虚拟机上的,自动是需要安装JVM环境的。更具体的安装可以参考官网方法。如果只是为了熟悉其语法,更便捷的办法是使用线上IDE学习。比如这个:https://groovyide.com/cpi变量定义关键字def:def str = "hello world";// 定义一个字符串 def str2 = "${str}!"; // 字符串支持插值 println(str2); // 一个跨行长字符串,不支持插值 def str3 = ''' 1)正式后台 2)测试后台 '''; print(str3.replaceAll('\\s+','').trim());查看动态类型信息和断言使用 // 查看动态类型信息和断言使用 def mixed = 1; assert mixed.class == java.lang.Integer; mixed = "str"; assert mixed.class == java.lang.String; //函数调用时,参数可以不使用括号 println mixed.class.name assert mixed.class == java.lang.Integer:"not a integer" assert mixed.getClass() == java.util.HashMap : "not valid select" assert backends instanceof java.lang.Integer : "not a Integer"支持Java写法, 注意数据和字典都是使用[]符号定义,只是后者还需要使用:指定valuedef dict = ['name':'john','age':14,'sex':'boy']; //println dict dict.each{ def kv = String.format("%s == %s", it.getKey(), it.getValue()); println kv; }支持闭包
2021年06月26日
68 阅读
0 评论
0 点赞
1
...
9
10
11
12