调用_c#调用click后不再调用调用mouseleave - CSDN
  • 函数调用过程

    2018-01-31 09:31:13
    今天突然看到有人私信我说一直没写函数调用过程(栈帧的形成和销毁过程)这篇博文,赶紧补上。 刚看的栈帧内容时,我很迷惑,我觉得栈帧创建和销毁很麻烦,几句话根本说不完,而且我好像描述不清楚他的过程,所以...

    今天突然看到有人私信我说一直没写函数调用过程(栈帧的形成和销毁过程)这篇博文,赶紧补上。
    刚看的栈帧内容时,我很迷惑,我觉得栈帧创建和销毁很麻烦,几句话根本说不完,而且我好像描述不清楚他的过程,所以在博文里面遇到函数调用我就规避了。现在再写栈帧调用过程,我觉得其实这个过程没有那么困难(不过还是有些抽象,毕竟计算机底层怎么运行我们也不是很明白)。
    栈帧的创建的销毁过程例子代码:

    int Add(int x,int y)
    {
        int sum = 0;
        sum = x + y;
        return sum;
    }
    
    int main ()
    {
        int a = 10;
        int b = 12;
        int ret = 0;
        ret = Add(a,b);
        return 0;
    }
    

    今天主要用汇编代码去讲述这个过程,首先介绍几个寄存器和简单的汇编指令的意思。
    先看几个函数调用过程涉及到的寄存器:
    (1)esp:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
    (2)ebp:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
    (3)eax 是”累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器。
    (4)ebx 是”基地址”(base)寄存器, 在内存寻址时存放基地址。
    (5)ecx 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。
    (6)edx 则总是被用来放整数除法产生的余数。
    (7)esi/edi分别叫做”源/目标索引寄存器”(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.
    在32位平台上,ESP每次减少4字节。
    再看几条简单的汇编指令:
    mov :数据传送指令,也是最基本的编程指令,用于将一个数据从源地址传送到目标地址(寄存器间的数据传送本质上也是一样的)
    sub:减法指令
    lea:取偏移地址
    push:实现压入操作的指令是PUSH指令
    pop:实现弹出操作的指令
    call:用于保存当前指令的下一条指令并跳转到目标函数。
    这些指令当然能看懂最好,可以让你很深刻的理解函数调用过程,不能看懂就只能通过我的描述去理解了。
    进行分析之前,先来了解下内存地址空间的分布:
    进程地址空间分布
    栈空间是向低地址增长的,主要是用来保存函数栈帧。 栈空间的大小很有限,仅有区区几MB大小
    汇编代码实现:
    main函数汇编代码:

    int main ()
    {
    011B26E0  push        ebp  
    011B26E1  mov         ebp,esp 
    011B26E3  sub         esp,0E4h 
    011B26E9  push        ebx  
    011B26EA  push        esi  
    011B26EB  push        edi  
    011B26EC  lea         edi,[ebp-0E4h] 
    011B26F2  mov         ecx,39h 
    011B26F7  mov         eax,0CCCCCCCCh 
    011B26FC  rep stos    dword ptr es:[edi] 
        int a = 10;
    011B26FE  mov         dword ptr [a],0Ah 
        int b = 12;
    011B2705  mov         dword ptr [b],0Ch 
        int ret = 0;
    011B270C  mov         dword ptr [ret],0 
        ret = Add(a,b);
    011B2713  mov         eax,dword ptr [b] 
    011B2716  push        eax  
    011B2717  mov         ecx,dword ptr [a] 
    011B271A  push        ecx  
    011B271B  call        @ILT+640(_Add) (11B1285h) 
    011B2720  add         esp,8 
    011B2723  mov         dword ptr [ret],eax 
        return 0;
    011B2726  xor         eax,eax 
    }
    011B2728  pop         edi  
    011B2729  pop         esi  
    011B272A  pop         ebx  
    011B272B  add         esp,0E4h 
    011B2731  cmp         ebp,esp 
    011B2733  call        @ILT+450(__RTC_CheckEsp) (11B11C7h) 
    011B2738  mov         esp,ebp 
    011B273A  pop         ebp  
    011B273B  ret              

    Add函数汇编代码:

    int Add(int x,int y)
    {
    011B26A0  push        ebp  
    011B26A1  mov         ebp,esp 
    011B26A3  sub         esp,0CCh 
    011B26A9  push        ebx  
    011B26AA  push        esi  
    011B26AB  push        edi  
    011B26AC  lea         edi,[ebp-0CCh] 
    011B26B2  mov         ecx,33h 
    011B26B7  mov         eax,0CCCCCCCCh 
    011B26BC  rep stos    dword ptr es:[edi] 
        int sum = 0;
    011B26BE  mov         dword ptr [sum],0 
        sum = x + y;
    011B26C5  mov         eax,dword ptr [x] 
    011B26C8  add         eax,dword ptr [y] 
    011B26CB  mov         dword ptr [sum],eax 
        return sum;
    011B26CE  mov         eax,dword ptr [sum] 
    }
    011B26D1  pop         edi  
    011B26D2  pop         esi  
    011B26D3  pop         ebx  
    011B26D4  mov         esp,ebp 
    011B26D6  pop         ebp  
    011B26D7  ret              

    。 下面图中详细描述了调用过程地址变化(此处所有地址是取自32位windows系统vs编辑器下的调试过程。):
    函数调用过程
    过程描述:
    1、参数拷贝(参数实例化)。
    2、保存当前指令的下一条指令,并跳转到被调函数。
    这些操作均在main函数中进行。

    接下来是调用Add函数并执行的一些操作,包括:
    1、移动ebp、esp形成新的栈帧结构。
    2、压栈(push)形成临时变量并执行相关操作。
    3、return一个值。
    这些操作在Add函数中进行。

    被调函数完成相关操作后需返回到原函数中执行下一条指令,操作如下:
    1、出栈(pop)。
    2、回复main函数的栈帧结构。(pop )
    3、返回main函数
    这些操作也在Add函数中进行。 至此,在main函数中调用Add函数的整个过程已经完成。
    总结起来整个过程就三步:
    1)根据调用的函数名找到函数入口;
    2)在栈中审请调用函数中的参数及函数体内定义的变量的内存空间
    3)函数执行完后,释放函数在栈中的审请的参数和变量的空间,最后返回值(如果有的话)
    如果你学了微机原理,你会想到cpu中断处理过程,是的,函数调用过程和中断处理过程一模一样。

    函数调用约定:
    这里再补充一下各种调用规定的基本内容。
    _stdcall调用约定

    所有参数按照从右到左压入堆栈,由被调用的子程序清理堆栈

    _cdecl调用约定(The C default calling convention,C调用规定)

    参数也是从右到左压入堆栈,但由调用者清理堆栈。

    _fastcall调用约定

    顾名思义,_fastcall的目的主要是为了更快的调用函数。它主要依靠寄存器传递参数,剩下的参数依然按照从右到左的顺序压入堆栈,并由被调用的子程序清理堆栈。

    本篇博文是按调用约定__stdcall 调用函数。

    展开全文
  • 然后,我们考察了Linux内核如何实现系统调用,以及执行系统调用的连锁反应:陷入内核,传递系统调用号和参数,执行正确的系统调用函数,并把返回值带回用户空间。最后讨论了如何增加系统调用,并提供了从用户空间...

    本文介绍了系统调用的一些实现细节。首先分析了系统调用的意义,它们与库函数和应用程序接口(API)有怎样的关系。然后,我们考察了Linux内核如何实现系统调用,以及执行系统调用的连锁反应:陷入内核,传递系统调用号和参数,执行正确的系统调用函数,并把返回值带回用户空间。最后讨论了如何增加系统调用,并提供了从用户空间访问系统调用的简单例子。
    参考 《Linux内核设计与实现》读书笔记(五)- 系统调用

    系统调用概述


    计算机系统的各种硬件资源是有限的,在现代多任务操作系统上同时运行的多个进程都需要访问这些资源,为了更好的管理这些资源进程是不允许直接操作的,所有对这些资源的访问都必须有操作系统控制。也就是说操作系统是使用这些资源的唯一入口,而这个入口就是操作系统提供的系统调用(System Call)。在linux中系统调用是用户空间访问内核的唯一手段,除异常和陷入外,他们是内核唯一的合法入口。

    一般情况下应用程序通过应用编程接口API,而不是直接通过系统调用来编程。在Unix世界,最流行的API是基于POSIX标准的。

    操作系统一般是通过中断从用户态切换到内核态。中断就是一个硬件或软件请求,要求CPU暂停当前的工作,去处理更重要的事情。比如,在x86机器上可以通过int指令进行软件中断,而在磁盘完成读写操作后会向CPU发起硬件中断。

    中断有两个重要的属性,中断号和中断处理程序。中断号用来标识不同的中断,不同的中断具有不同的中断处理程序。在操作系统内核中维护着一个中断向量表(Interrupt Vector Table),这个数组存储了所有中断处理程序的地址,而中断号就是相应中断在中断向量表中的偏移量。

    一般地,系统调用都是通过软件中断实现的,x86系统上的软件中断由int $0x80指令产生,而128号异常处理程序就是系统调用处理程序system_call(),它与硬件体系有关,在entry.S中用汇编写。接下来就来看一下Linux下系统调用具体的实现过程。

    为什么需要系统调用


    linux内核中设置了一组用于实现系统功能的子程序,称为系统调用。系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于内核态,而普通的函数调用由函数库或用户自己提供,运行于用户态

    一般的,进程是不能访问内核的。它不能访问内核所占内存空间也不能调用内核函数。CPU硬件决定了这些(这就是为什么它被称作“保护模式”(详细参见深入理解计算机系统-之-内存寻址(二)–存储保护机制(CPU实模式与保护模式)))。

    为了和用户空间上运行的进程进行交互,内核提供了一组接口。透过该接口,应用程序可以访问硬件设备和其他操作系统资源。这组接口在应用程序和内核之间扮演了使者的角色,应用程序发送各种请求,而内核负责满足这些请求(或者让应用程序暂时搁置)。实际上提供这组接口主要是为了保证系统稳定可靠,避免应用程序肆意妄行,惹出大麻烦。

    系统调用在用户空间进程和硬件设备之间添加了一个中间层。该层主要作用有三个:

    • 它为用户空间提供了一种统一的硬件的抽象接口。比如当需要读些文件的时候,应用程序就可以不去管磁盘类型和介质,甚至不用去管文件所在的文件系统到底是哪种类型。

    • 系统调用保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核可以基于权限和其他一些规则对需要进行的访问进行裁决。举例来说,这样可以避免应用程序不正确地使用硬件设备,窃取其他进程的资源,或做出其他什么危害系统的事情。

    • 每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是出于这种考虑。如果应用程序可以随意访问硬件而内核又对此一无所知的话,几乎就没法实现多任务和虚拟内存,当然也不可能实现良好的稳定性和安全性。在Linux中,系统调用是用户空间访问内核的惟一手段;除异常和中断外,它们是内核惟一的合法入口。

    API/POSIX/C库的区别与联系


    一般情况下,应用程序通过应用编程接口(API)而不是直接通过系统调用来编程。这点很重要,因为应用程序使用的这种编程接口实际上并不需要和内核提供的系统调用一一对应。

    一个API定义了一组应用程序使用的编程接口。它们可以实现成一个系统调用,也可以通过调用多个系统调用来实现,而完全不使用任何系统调用也不存在问题。实际上,API可以在各种不同的操作系统上实现,给应用程序提供完全相同的接口,而它们本身在这些系统上的实现却可能迥异。

    在Unix世界中,最流行的应用编程接口是基于POSIX标准的,其目标是提供一套大体上基于Unix的可移植操作系统标准。POSIX是说明API和系统调用之间关系的一个极好例子。在大多数Unix系统上,根据POSIX而定义的API函数和系统调用之间有着直接关系。

    Linux的系统调用像大多数Unix系统一样,作为C库的一部分提供如下图所示。C库实现了 Unix系统的主要API,包括标准C库函数和系统调用。所有的C程序都可以使用C库,而由于C语言本身的特点,其他语言也可以很方便地把它们封装起来使用。

    从程序员的角度看,系统调用无关紧要,他们只需要跟API打交道就可以了。相反,内核只跟系统调用打交道;库函数及应用程序是怎么使用系统调用不是内核所关心的。

    关于Unix的界面设计有一句通用的格言“提供机制而不是策略”。换句话说,Unix的系统调用抽象出了用于完成某种确定目的的函数。至干这些函数怎么用完全不需要内核去关心。区别对待机制(mechanism)和策略(policy)是Unix设计中的一大亮点。大部分的编程问题都可以被切割成两个部分:“需要提供什么功能”(机制)和“怎样实现这些功能”(策略)。

    区别


    api是函数的定义,规定了这个函数的功能,跟内核无直接关系。而系统调用是通过中断向内核发请求,实现内核提供的某些服务。

    联系


    一个api可能会需要一个或多个系统调用来完成特定功能。通俗点说就是如果这个api需要跟内核打交道就需要系统调用,否则不需要。
    程序员调用的是API(API函数),然后通过与系统调用共同完成函数的功能。
    因此,API是一个提供给应用程序的接口,一组函数,是与程序员进行直接交互的。
    系统调用则不与程序员进行交互的,它根据API函数,通过一个软中断机制向内核提交请求,以获取内核服务的接口。
    并不是所有的API函数都一一对应一个系统调用,有时,一个API函数会需要几个系统调用来共同完成函数的功能,甚至还有一些API函数不需要调用相应的系统调用(因此它所完成的不是内核提供的服务)

    系统调用的实现原理


    基本机制


    前文已经提到了Linux下的系统调用是通过0x80实现的,但是我们知道操作系统会有多个系统调用(Linux下有319个系统调用),而对于同一个中断号是如何处理多个不同的系统调用的?最简单的方式是对于不同的系统调用采用不同的中断号,但是中断号明显是一种稀缺资源,Linux显然不会这么做;还有一个问题就是系统调用是需要提供参数,并且具有返回值的,这些参数又是怎么传递的?也就是说,对于系统调用我们要搞清楚两点:

    1. 系统调用的函数名称转换。
    2. 系统调用的参数传递。

    首先看第一个问题。实际上,Linux中每个系统调用都有相应的系统调用号作为唯一的标识,内核维护一张系统调用表,sys_call_table,表中的元素是系统调用函数的起始地址,而系统调用号就是系统调用在调用表的偏移量。在x86上,系统调用号是通过eax寄存器传递给内核的。比如fork()的实现:

    用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统安全就会失去控制。所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。

    通知内核的机制是靠软件中断实现的。首先,用户程序为系统调用设置参数。其中一个参数是系统调用编号。参数设置完成后,程序执行“系统调用”指令。x86系统上的软中断由int产生。这个指令会导致一个异常:产生一个事件,这个事件会致使处理器切换到内核态并跳转到一个新的地址,并开始执行那里的异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。它与硬件体系结构紧密相关。

    新地址的指令会保存程序的状态,计算出应该调用哪个系统调用,调用内核中实现那个系统调用的函数,恢复用户程序状态,然后将控制权返还给用户程序。系统调用是设备驱动程序中定义的函数最终被调用的一种方式。

    从系统分析的角度,linux的系统调用涉及4个方面的问题。

    响应函数sys_xxx


    响应函数名以“sys_”开头,后跟该系统调用的名字。

    例如

    系统调用fork()的响应函数是sys_fork()(见Kernel/fork.c),

    exit()的响应函数是sys_exit()(见kernel/fork.)。

    系统调用表与系统调用号-=>数组与下标


    文件include/asm/unisted.h为每个系统调用规定了唯一的编号。
    系统调用号

    在我们系统中/usr/include/asm/unistd_32.h,可以通过find / -name unistd_32.h -print查找)
    而内核中的头文件路径不同的内核版本以及不同的发行版,文件的存储结构可能有所区别

    linux-3.0
    这里写图片描述

    linux-2.6

    这里写图片描述

    假设用name表示系统调用的名称,那么系统调用号与系统调用响应函数的关系是:以系统调用号_NR_name作为下标,可找出系统调用表sys_call_table(见arch/i386/kernel/entry.S)中对应表项的内容,它正好是该系统调用的响应函数sys_name的入口地址。

    系统调用表sys_call_table记录了各sys_name函数在表中的位置,共190项。有了这张表,就很容易根据特定系统调用

    sys_call_table

    在表中的偏移量,找到对应的系统调用响应函数的入口地址。系统调用表共256项,余下的项是可供用户自己添加的系统调用空间。

    在Linux中,每个系统调用被赋予一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行哪个系统调用。进程不会提及系统调用的名称。

    系统调用号相当关键,一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃。Linux有一个“未实现”系统调用sys_ni_syscall(),它除了返回一ENOSYS外不做任何其他工作,这个错误号就是专门针对无效的系统调用而设的。

    因为所有的系统调用陷入内核的方式都一样,所以仅仅是陷入内核空间是不够的。因此必须把系统调用号一并传给内核。在x86上,系统调用号是通过eax寄存器传递给内核的。在陷人内核之前,用户空间就把相应系统调用所对应的号放入eax中了。这样系统调用处理程序一旦运行,就可以从eax中得到数据。其他体系结构上的实现也都类似。

    内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在sys_call_table中。它与体系结构有关,一般在entry.s中定义。这个表中为每一个有效的系统调用指定了惟一的系统调用号。sys_call_table是一张由指向实现各种系统调用的内核函数的函数指针组成的表:
    system_call()函数通过将给定的系统调用号与NR_syscalls做比较来检查其有效性。如果它大于或者等于NR syscalls,该函数就返回一ENOSYS。否则,就执行相应的系统调用。
    这里写图片描述

          call *sys_ call-table(,%eax, 4)

    由于系统调用表中的表项是以32位(4字节)类型存放的,所以内核需要将给定的系统调用号乘以4,然后用所得的结果在该表中查询其位置

    进程的系统调用命令转换为INT 0x80中断的过程


    宏定义_syscallN()include/asm/unisted.h)用于系统调用的格式转换和参数的传递。N取0~5之间的整数。

    参数个数为N的系统调用由_syscallN()负责格式转换和参数传递。系统调用号放入EAX寄存器,启动INT 0x80后,规定返回值送EAX寄存器。

    系统调用功能模块的初始化


    对系统调用的初始化也就是对INT 0x80的初始化。

    系统启动时,汇编子程序setup_idt(见arch/i386/kernel/head.S)准备了1张256项的idt表,由start_kernel()(见init/main.c),trap_init()(见arch/i386/kernel/traps.c)调用的C语言宏定义set_system_gate(0x80,&system_call)(见include/asm/system.h)设置0x80号软中断的服务程序为 system_call(见arch/i386/kernel/entry.S), system.call就是所有系统调用的总入口。

    内核如何为各种系统调用服务


    当进程需要进行系统调用时,必须以C语言函数的形式写一句系统调用命令。该命令如果已在某个头文件中由相应的_syscallN()展开,则用户程序必须包含该文件。当进程执行到用户程序的系统调用命令时,实际上执行了由宏命令_syscallN()展开的函数。系统调用的参数 由各通用寄存器传递,然后执行INT 0x80,以内核态进入入口地址system_call

    ret_from_sys_call


    ret_from_sys_call入口的汇编程序段在linux进程管理中起到了十分重要的作用。

    所有系统调用结束前以及大部分中断服务返回前,都会跳转至此处入口地址。 该段程序不仅仅为系统调用服务,它还处理中断嵌套、CPU调度、信号等事务。

    内核如何为系统调用的参数传递参数


    参数传递


    除了系统调用号以外,大部分系统调用都还需要一些外部的参数输人。所以,在发生异常的时候,应该把这些参数从用户空间传给内核。最简单的办法就是像传递系统调用号一样把这些参数也存放在寄存器里。在x86系统上,ebx, ecx, edx, esiedi按照顺序存放前五个参数。需要六个或六个以上参数的情况不多见,此时,应该用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。

    给用户空间的返回值也通过寄存器传递。在x86系统上,它存放在eax寄存器中。接下来许多关于系统调用处理程序的描述都是针对x86版本的。但不用担心,所有体系结构的实现都很类似。

    参数验证


    系统调用必须仔细检查它们所有的参数是否合法有效。举例来说,与文件I/O相关的系统调用必须检查文件描述符是否有效。与进程相关的函数必须检查提供的PID是否有效。必须检查每个参数,保证它们不但合法有效,而且正确。

    最重要的一种检查就是检查用户提供的指针是否有效。试想,如果一个进程可以给内核传递指针而又无须被检查,那么它就可以给出一个它根本就没有访问权限的指针,哄骗内核去为它拷贝本不允许它访问的数据,如原本属于其他进程的数据。在接收一个用户空间的指针之前,内核必须保证:

    • 指针指向的内存区域属于用户空间。进程决不能哄骗内核去读内核空间的数据。

    • 指针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其他进程的数据。

    • 如果是读,该内存应被标记为可读。如果是写,该内存应被标记为可写。进程决不能绕过内存访问限制。

    内核提供了两个方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。注意,内核无论何时都不能轻率地接受来自用户空间的指针!这两个方法中必须有一个被调用。为了向用户空间写入数据,内核提供了copy_to_user(),它需要三个参数。第一个参数是进程空间中的目的内存地址。第二个是内核空间内的源地址。最后一个参数是需要拷贝的数据长度(字节数)。

    为了从用户空间读取数据,内核提供了copy_from_ user(),它和copy-to-User()相似。该函数把第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据长度由第三个参数决定。

    如果执行失败,这两个函数返回的都是没能完成拷贝的数据的字节数。如果成功,返回0。当出现上述错误时,系统调用返回标准-EFAULT。

    注意copy_to_user()copy_from_user()都有可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。此时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。

    系统调用的返回值


    系统调用(在Linux中常称作syscalls)通常通过函数进行调用。它们通常都需要定义一个或几个参数(输入)而且可能产生一些副作用,例如写某个文件或向给定的指针拷贝数据等等。为防止和正常的返回值混淆,系统调用并不直接返回错误码,而是将错误码放入一个名为errno的全局变量中。通常用一个负的返回值来表明错误。返回一个0值通常表明成功。如果一个系统调用失败,你可以读出errno的值来确定问题所在。通过调用perror()库函数,可以把该变量翻译成用户可以理解的错误字符串。

    errno不同数值所代表的错误消息定义在errno.h中,你也可以通过命令”man 3 errno”来察看它们。需要注意的是,errno的值只在函数发生错误时设置,如果函数不发生错误,errno的值就无定义,并不会被置为0。另外,在处理errno前最好先把它的值存入另一个变量,因为在错误处理过程中,即使像printf()这样的函数出错时也会改变errno的值。

    当然,系统调用最终具有一种明确的操作。举例来说,如getpid()系统调用,根据定义它会返回当前进程的PID。内核中它的实现非常简单:

    asmlinkage long sys_ getpid(void)
    {
        return current-> tgid;
    }

    上述的系统调用尽管非常简单,但我们还是可以从中发现两个特别之处。首先,注意函数声明中的asmlinkage限定词,这是一个小戏法,用于通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词。其次,注意系统调用get_pid()在内核中被定义成sys_ getpid。这是Linux中所有系统调用都应该遵守的命名规则。

    访问系统调用


    系统调用上下文


    内核在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程。

    在进程上下文中,内核可以休眠并且可以被抢占。这两点都很重要。首先,能够休眠说明系统调用可以使用内核提供的绝大部分功能。休眠的能力会给内核编程带来极大便利。在进程上下文中能够被抢占,其实表明,像用户空间内的进程一样,当前的进程同样可以被其他进程抢占。因为新的进程可以使用相同的系统调用,所以必须小心,保证该系统调用是可重人的。当然,这也是在对称多处理中必须同样关心的问题。

    当系统调用返回的时候,控制权仍然在system_call()中,它最终会负责切换到用户空间并让用户进程继续执行下去。

    系统调用访问示例


    操作系统使用系统调用表将系统调用编号翻译为特定的系统调用。系统调用表包含有实现每个系统调用的函数的地址。例如,read() 系统调用函数名为sys_readread()系统调用编号是 3,所以sys_read() 位于系统调用表的第四个条目中(因为系统调用起始编号为0)。从地址 sys_call_table + (3 * word_size) 读取数据,得到sys_read()的地址。

    找到正确的系统调用地址后,它将控制权转交给那个系统调用。我们来看定义sys_read()的位置,即fs/read_write.c文件。这个函数会找到关联到 fd 编号(传递给 read() 函数的)的文件结构体。那个结构体包含指向用来读取特定类型文件数据的函数的指针。进行一些检查后,它调用与文件相关的 read() 函数,来真正从文件中读取数据并返回。与文件相关的函数是在其他地方定义的 —— 比如套接字代码、文件系统代码,或者设备驱动程序代码。这是特定内核子系统最终与内核其他部分协作的一个方面。

    读取函数结束后,从sys_read()返回,它将控制权切换给 ret_from_sys。它会去检查那些在切换回用户空间之前需要完成的任务。如果没有需要做的事情,那么就恢复用户进程的状态,并将控制权交还给用户程序。

    从用户空间直接访问系统调用


    通常,系统调用靠C库支持。用户程序通过包含标准头文件并和C库链接,就可以使用系统调用(或者调用库函数,再由库函数实际调用)。但如果你仅仅写出系统调用,glibc库恐怕并不提供支持。值得庆幸的是,Linux本身提供了一组宏,用于直接对系统调用进行访问。它会设置好寄存器并调用陷人指令。这些宏是_syscalln(),其中n的范围从0到6。代表需要传递给系统调用的参数个数,这是由于该宏必须了解到底有多少参数按照什么次序压入寄存器。举个例子,open()系统调用的定义是:

    long open(const char *filename, int flags, int mode)

    而不靠库支持,直接调用此系统调用的宏的形式为:

    #define NR_ open 5
    syscall3(long, open, const char*,filename, int, flags, int, mode)

    这样,应用程序就可以直接使用open()
    对于每个宏来说,都有2+ n个参数。
    第一个参数对应着系统调用的返回值类型。
    第二个参数是系统调用的名称。再以后是按照系统调用参数的顺序排列的每个参数的类型和名称。
    _NR_ open<asm/unistd.h>中定义,是系统调用号。该宏会被扩展成为内嵌汇编的C函数。由汇编语言执行前一节所讨论的步骤,将系统调用号和参数压入寄存器并触发软中断来陷入内核。调用open()系统调用直接把上面的宏放置在应用程序中就可以了。

    让我们写一个宏来使用前面编写的foo()系统调用,然后再写出测试代码炫耀一下我们所做的努力。

    #define NR foo 283
    _sysca110(long, foo)
    int main()
    {
    long stack size;
    stack_ size=foo();
    printf("The kernel stack
    size is 81d/n",stack_ size);
    return;
    }

    添加系统调用


    通过修改内核源代码添加系统调用


    linux-2.6.*


    通过以上分析linux系统调用的过程,

    将自己的系统调用加到内核中就是一件容易的事情。下面介绍一个实际的系统调用,

    并把它加到内核中去。要增加的系统调用是:inttestsyscall(),其功能是在控制终端屏幕上显示hello world,

    执行成功后返回0。

    编写int testsyscall()系统调用–响应函数


    编写一个系统调用意味着要给内核增加1个函数,将新函数放入文件kernel/sys.c中。新函数代码如下:

    asmlingkage sys_testsyscall()
    { 
        print("hello world\n");    
        return 0;
     }

    添加系统调用号


    编写了新的系统调用过程后,下一项任务是使内核的其余部分知道这一程序的存在,然后重建包含新的系统调用的内核。为了把新的函数连接到已有的内核中去, 需要编辑2个文件:

    1).inculde/asm/unistd.h在这个文件中加入

    #define_NR_testsyscall 191

    系统调用表中添加对应项


    2).are/i386/kernel/entry.s这个文件用来对指针数组初始化,在这个文件中增加一行:

     .long SYMBOL_NAME(_sys_tsetsycall)

    .rept NR_syscalls-190改为NR_SYSCALLS-191,然后重新编译和运行新内核。

    使用新的系统调用


    在保证的C语言库中没有新的系统调用的程序段,必须自己建立其代码如下

    #inculde
    
    _syscall0(int,testsyscall)
    
    main()
    {
        tsetsyscall();
    }

    在这里使用了_syscall0宏指令,宏指令本身在程序中将扩展成名为syscall()的函数,它在main()函数内部加以调用。

    testsyscall()函数中, 预处理程序产生所有必要的机器指令代码,包括用系统调用参数值加载相应的cpu寄存器, 然后执行int 0x80中断指令。

    linux-3.*


    在linux-3.8.4/kernel/sys.c 文件末尾添加新的系统调用函数如:

    asmlinkage int sys_mycall(int number)
    {    
        printk("这是我添加的第一个系统调用");    
        return number; }

    arch/x86/syscall_32.tbl下找到unused 223号调用然后替换如:

    223 i386 mycall sys_mycall

    如果是64位系统,在arch/x86/syscalls/syscall_64.tbl下找到313号系统调用,然后在其下面加上314号自己的中断如:
    `314 common mycall sys_mycall

    利用内核模块添加系统调用


    模块是内核的一部分,但是并没有被编译到内核里面去。它们被分别编译并连接成一组目标文件, 这些文件能被插入到正在运行的内核,或者从正在运行的内核中移走。内核模块至少必须有2个函数:
    

    init_modulecleanup_module

    第一个函数是在把模块插入内核时调用的;

    第二个函数则在删除该模块时调用。由于内核模块是内核的一部分,所以能访问所有内核资源。根据对linux系统调用机制的分析,

    如果要增加系统调用,可以编写自己的函数来实现,然后在sys_call_table表中增加一项,使该项中的指针指向自己编写的函数,

    就可以实现系统调用。下面用该方法实现在控制终端上打印“hello world” 的系统调用testsyscall()。

    编写系统调用内核模块


    #inculde(linux/kernel.h)
    
    #inculde(linux/module.h)
    
    #inculde(linux/modversions.h)
    
    #inculde(linux/sched.h)
    
     #inculde(asm/uaccess.h)
    
    #define_NR_testsyscall 191
    
    extern viod *sys_call+table[];
    
    asmlinkage int testsyscall()
    
    { 
        printf("hello world\n");
    
        return 0;
    
    }
    
    int init_module()
    
    { 
        sys_call_table[_NR_tsetsyscall]=testsyscall;
        printf("system call testsyscall() loaded success\n");
    
        return 0;
    }
    
    void cleanup_module()
    {
    
    }

    使用新的系统调用

    #define_NR_testsyscall 191
    
    _syscall0(int,testsyscall)
    
    main()
    {
        testsyscall();
    }

    内核Linux系统调用的列表


    以下是Linux系统调用的一个列表,包含了大部分常用系统调用和由系统调用派生出的的函数。

    进程控制


    系统调用 描述
    fork 创建一个新进程
    clone 按指定条件创建子进程
    execve 运行可执行文件
    exit 中止进程
    _exit 立即中止当前进程
    getdtablesize 进程所能打开的最大文件数
    getpgid 获取指定进程组标识号
    setpgid 设置指定进程组标志号
    getpgrp 获取当前进程组标识号
    setpgrp 设置当前进程组标志号
    getpid 获取进程标识号
    getppid 获取父进程标识号
    getpriority 获取调度优先级
    setpriority 设置调度优先级
    modify_ldt 读写进程的本地描述表
    nanosleep 使进程睡眠指定的时间
    nice 改变分时进程的优先级
    pause 挂起进程,等待信号
    personality 设置进程运行域
    prctl 对进程进行特定操作
    ptrace 进程跟踪
    sched_get_priority_max 取得静态优先级的上限
    sched_get_priority_min 取得静态优先级的下限
    sched_getparam 取得进程的调度参数
    sched_getscheduler 取得指定进程的调度策略
    sched_rr_get_interval 取得按RR算法调度的实时进程的时间片长度
    sched_setparam 设置进程的调度参数
    sched_setscheduler 设置指定进程的调度策略和参数
    sched_yield 进程主动让出处理器,并将自己等候调度队列队尾
    vfork 创建一个子进程,以供执行新程序,常与execve等同时使用
    wait 等待子进程终止
    wait3 参见wait
    waitpid 等待指定子进程终止
    wait4 参见waitpid
    capget 获取进程权限
    capset 设置进程权限
    getsid 获取会晤标识号
    setsid 设置会晤标识号

    文件系统控制


    文件读写操作


    系统调用 描述
    fcntl 文件控制
    open 打开文件
    creat 创建新文件
    close 关闭文件描述字
    read 读文件
    write 写文件
    readv 从文件读入数据到缓冲数组中
    writev 将缓冲数组里的数据写入文件
    pread 对文件随机读
    pwrite 对文件随机写
    lseek 移动文件指针
    _llseek 在64位地址空间里移动文件指针
    dup 复制已打开的文件描述字
    dup2 按指定条件复制文件描述字
    flock 文件加/解锁
    poll I/O多路转换
    truncat e 截断文件
    ftruncate 参见truncate
    vumask 设置文件权限掩码
    fsync 把文件在内存中的部分写回磁盘

    文件系统操作


    系统调用 描述
    access 确定文件的可存取性
    chdir 改变当前工作目录
    fchdir 参见chdir
    chmod 改变文件方式
    fchmod 参见chmod
    chown 改变文件的属主或用户组
    fchown 参见chown
    lchown 参见chown
    chroot 改变根目录
    stat 取文件状态信息
    lstat 参见stat
    fstat 参见stat
    statfs 取文件系统信息
    fstatfs 参见statfs
    readdir 读取目录项
    getdents 读取目录项
    mkdir 创建目录
    mknod 创建索引节点
    rmdir 删除目录
    rename 文件改名
    link 创建链接
    symlink 创建符号链接
    unlink 删除链接
    readlink 读符号链接的值
    mount 安装文件系统
    umount 卸下文件系统
    ustat 取文件系统信息
    utime 改变文件的访问修改时间
    utimes 参见utime
    quotactl 控制磁盘配额

    系统控制


    系统调用 描述
    ioctl I/O总控制函数
    _sysctl 读/写系统参数
    acct 启用或禁止进程记账
    getrlimit 获取系统资源上限
    setrlimit 设置系统资源上限
    getrusage 获取系统资源使用情况
    uselib 选择要使用的二进制函数库
    ioperm 设置端口I/O权限
    iopl 改变进程I/O权限级别
    outb 低级端口操作
    reboot 重新启动
    swapon 打开交换文件和设备
    swapoff 关闭交换文件和设备
    bdflush 控制bdflush守护进程
    sysfs 取核心支持的文件系统类型
    sysinfo 取得系统信息
    adjtimex 调整系统时钟
    alarm 设置进程的闹钟
    getitimer 获取计时器值
    setitimer 设置计时器值
    gettimeofday 取时间和时区
    settimeofday 设置时间和时区
    stime 设置系统日期和时间
    time 取得系统时间
    times 取进程运行时间
    uname 获取当前UNIX系统的名称、版本和主机等信息
    vhangup 挂起当前终端
    nfsservctl 对NFS守护进程进行控制
    vm86 进入模拟8086模式
    create_module 创建可装载的模块项
    delete_module 删除可装载的模块项
    init_module 初始化模块
    query_module 查询模块信息
    *get_kernel_syms 取得核心符号,已被query_module代替

    内存管理


    系统调用 描述
    brk 改变数据段空间的分配
    sbrk 参见brk
    mlock 内存页面加锁
    munlock 内存页面解锁
    mlockall 调用进程所有内存页面加锁
    munlockall 调用进程所有内存页面解锁
    mmap 映射虚拟内存页
    munmap 去除内存页映射
    mremap 重新映射虚拟内存地址
    msync 将映射内存中的数据写回磁盘
    mprotect 设置内存映像保护
    getpagesize 获取页面大小
    sync 将内存缓冲区数据写回硬盘
    cacheflush 将指定缓冲区中的内容写回磁盘

    网络管理


    系统调用 描述
    getdomainname 取域名
    setdomainname 设置域名
    gethostid 获取主机标识号
    sethostid 设置主机标识号
    gethostname 获取本主机名称
    sethostname 设置主机名称

    socket控制


    系统调用 描述
    socketcall socket系统调用
    socket 建立socket
    bind 绑定socket到端口
    connect 连接远程主机
    accept 响应socket连接请求
    send 通过socket发送信息
    sendto 发送UDP信息
    sendmsg 参见send
    recv 通过socket接收信息
    recvfrom 接收UDP信息
    recvmsg 参见recv
    listen 监听socket端口
    select 对多路同步I/O进行轮询
    shutdown 关闭socket上的连接
    getsockname 取得本地socket名字
    getpeername 获取通信对方的socket名字
    getsockopt 取端口设置
    setsockopt 设置端口参数
    sendfile 在文件或端口间传输数据
    socketpair 创建一对已联接的无名socket

    用户管理


    系统调用 描述
    getuid 获取用户标识号
    setuid 设置用户标志号
    getgid 获取组标识号
    setgid 设置组标志号
    getegid 获取有效组标识号
    setegid 设置有效组标识号
    geteuid 获取有效用户标识号
    seteuid 设置有效用户标识号
    setregid 分别设置真实和有效的的组标识号
    setreuid 分别设置真实和有效的用户标识号
    getresgid 分别获取真实的,有效的和保存过的组标识号
    setresgid 分别设置真实的,有效的和保存过的组标识号
    getresuid 分别获取真实的,有效的和保存过的用户标识号
    setresuid 分别设置真实的,有效的和保存过的用户标识号
    setfsgid 设置文件系统检查时使用的组标识号
    setfsuid 设置文件系统检查时使用的用户标识号
    getgroups 获取后补组标志清单
    setgroups 设置后补组标志清单

    进程间通信


    系统调用 描述
    ipc 进程间通信总控制调用

    信号


    系统调用 描述
    sigaction 设置对指定信号的处理方法
    sigprocmask 根据参数对信号集中的信号执行阻塞/解除阻塞等操作
    sigpending 为指定的被阻塞信号设置队列
    sigsuspend 挂起进程等待特定信号
    signal 参见signal
    kill 向进程或进程组发信号
    *sigblock 向被阻塞信号掩码中添加信号,已被sigprocmask代替
    *siggetmask 取得现有阻塞信号掩码,已被sigprocmask代替
    *sigsetmask 用给定信号掩码替换现有阻塞信号掩码,已被sigprocmask代替
    *sigmask 将给定的信号转化为掩码,已被sigprocmask代替
    *sigpause 作用同sigsuspend,已被sigsuspend代替
    sigvec 为兼容BSD而设的信号处理函数,作用类似sigaction
    ssetmask ANSI C的信号处理函数,作用类似sigaction

    消息


    系统调用 描述
    msgctl 消息控制操作
    msgget 获取消息队列
    msgsnd 发消息
    msgrcv 取消息

    管道


    系统调用 描述
    pipe 创建管道

    信号量


    系统调用 描述
    semctl 信号量控制
    semget 获取一组信号量
    semop 信号量操作

    共享内存


    系统调用 描述
    shmctl 控制共享内存
    shmget 获取共享内存
    shmat 连接共享内存
    shmdt 拆卸共享内存
    展开全文
  • 服务调用

    2020-01-01 14:32:04
    服务调用:除了常用的同步服务调用之外,分布式服务框架还需要支持其他几种形式的服务调用,下面将详细介绍。 一、常见误区 因惯性思维,很多人会将传统MVC架构或者RPC框架的做法带入到分布式服务框架的架构设计...

    服务调用:除了常用的同步服务调用之外,分布式服务框架还需要支持其他几种形式的服务调用,下面将详细介绍。

    一、常见误区


    因惯性思维,很多人会将传统MVC架构或者RPC框架的做法带入到分布式服务框架的架构设计中,其中有些思想存在误区,或者已过时,它们会破坏分布式服务架构的架构品质,下面将纠正这些误区。

    1.1、NIO 就是异步服务:实际上,通信框架基于 NIO实现,并不意味着服务框架就支持异步服务调用,两者本质上不是同一个层面的事情。在分布式服务框架中,引入NIO带来的好处是显而易见的,各种I/O对比如表1-1所示:

      同步阻塞I/O(BIO) 伪异步I/O 非阻塞I/O(NIO) 异步I/O(AIO)
    客户端个数:I/O线程 1:1 M:N(其中M可以大于N) M:1(1个I/O线程处理多个客户端连接)

    M:0(不需要启动额外的I/O线程,被动回调)

    I/O类型(阻塞) 阻塞(I/O) 阻塞(I/O) 非阻塞(I/O) 非阻塞(I/O)
    I/O类型(同步) 同步(I/O) 同步(I/O) 同步(I/O)(I/O多路复用) 异步(I/O)
    API使用难度 简单 简单 非常复杂 复杂
    调试难度 简单 简单 复杂 复杂
    可靠性 非常差

                                                       表1-1  几种I/O模型的功能和特性对比 

    引入NIO 的优点归纳如下
    【1】所有的 I/O操作都是非阻塞的,避免有限的 I/O线程因为网络、对方处理慢等原因被阻塞。
    【2】多路复用的 Reactor线程模式:基于 Linux的 epoll和 Selector,一个 I/O线程可以并行处理成百上千条链路,解决了传统同步I/O通信线程膨胀的问题。
    NIO只解决了通信层面的异步问题,跟服务调用的异步没有必然关系,也就是说,即便采用 BIO通信,依然可以实现异步服务调用,只不过通信效率和可靠性比较差而已。

    下面对异步服务调用和通信框架的关系进行说明,如图1-1所示
     
                                                图1-1   服务调用和通信框架的关系

    用户发起远程调用之后,经历了层层业务逻辑处理、消息编码,最总序列化后的消息会被放入到通信框架的消息队列中。业务线程可以选择同步等待、也可以选择直接返回,通过消息队列的方式实现业务层和通信层的分离是比较成熟、典型的做法,现代的RPC框架或者Web服务器很少直接使用业务线程进行网络读写。通过图1-1可以看出,采用 BIO还是 NIO对上层的业务是不可见的,双方的汇聚点是消息队列,在Java实现中它通常就是个 Queue。业务线程将消息放入到发送队列中,可以选择主动等待或者立即返回,跟通信系统是否是NIO没有任何关系。

    1.2、服务调用模式:【1】OneWay模式: 只有请求,没有应答,例如通知消息;
    【2】请求-应答模式:一请求,一应答的模式,这种模式最常见。

    OneWay 模式:服务调用由于不需要返回应答,因此很容易被设计为异步的:消费者发起远程服务调用之后,立即返回,不需要同步阻塞等待应答。
    对于请求-应答模式:一般的观点都认为消费者必须要等待服务端响应,拿到结果后才能返回,否则结果从哪里取?即便业务线程不阻塞,没有获取到结果流程还是无法继续执行下去。

    从逻辑上看,上述观点没有问题。但是实际中,同步阻塞等待应答并非是唯一的技术选择,我们也可以利用 Java的 Future-Listener 机制来实现异步服务调用。从业务角度看,它的效果与同步等待等价,但是从技术层面看,却是个很大的进步,他可以保证业务线程在不同步阻塞的情况下实现同步等待的效果,服务执行效率更高。

    1.3、异步服务调用性能更高:通常在实验室环境中测试,由于网络时延小、模拟业务又通常比较简单,所以异步服务调用并不一定性能更高,但是在生产环境中,异步服务调用往往性能更高、可靠性也更好。主要原因是网络环境相对恶劣,真时的服务调用耗时更多等,这种恶劣的运行环境正好能够发挥异步服务调用的优势。

    二、服务调用方式


    服务框架支持多种形式的服务调用,本节将对这集中服务调用的原理和设计进行讲解。

        2.1、同步服务调用:它的工作原理如下:客户端发起远程服务调用请求,用户线程完成消息序列化之后将消息投递到通信框架,然后同步阻塞,等待通信框架发送请求并接受应答之后,唤醒同步等待的用户线程,用户线程获取到应答之后返回。工作原理如图1-2所示:

                                                        图1-2  同步服务调用

    【1】消费者调用服务端发布的接口,接口调用由分布式服务框架包装成动态代理,发起远程服务调用。
    【2】消费者线程调用通信框架的消息发送接口之后,直接或者间接调用wait()方法,同步阻塞等待应答。
    【3】通信框架的 I/O线程通过网络将请求消息发送给服务端。
    【4】服务端返回应答消息给消费者,由通信框架负责应答消息的反序列化。
    【5】I/O线程获取到应答消息之后,根据消息上下文找到之前同步阻塞的业务线程,notify()阻塞的业务线程,返回应答给消费者,完成服务调用。

    为了防止服务端长时间不返回应答消息导致客户端用户线程挂死,用户线程等待的时候需要设置超时时间,这个超时时间与服务端或者客户端配置的超时时间对应。

    2.2、异步服务调用:基于 JDK 的 Future机制,可以非常方便地实现异步服务调用。

    JDK 原生的 Future主要用于异步操作,它代表了异步执行的结果,用户可以通过调用它的get方法获取结果。如果当前操作没有执行完,get 操作将阻塞调用线程。在实际项目中,往往会扩展 JDK 的 Future,提供 Future-Listener机制,它支持主动获取和被动异步回调通知两种模式,适用于不同的业务场景。

    以 Netty 的 Future 接口定义为例,新增了监听器管理接口,监听器主要用于异步通知回调。异步服务调用工作原理如图1-3所示:

                                  图1-3   异步服务调用工作原理

    【1】消费者调用服务端发布的接口,接口调用由分布式服务框架包装成动态代理,发起远程服务调用。
    【2】通信框架异步发送请求消息,如果没有发生 I/O异常,返回。
    【3】请求消息发送成功之后,I/O 线程构造 Future对象,设置到 RPC上下文中。
    【4】用户线程通过 RPC 上下文获取 Future对象。
    【5】构造 Listener对象,将其添加到 Future中,用于服务端应答异步回调通知。
    【6】用户线程返回,不阻塞等待应答。
    【7】服务端返回应答消息,通信框架负责反序列化。
    【8】I/O 线程将应答消息,设置到 Future对象的操作结果中。
    【9】Future 对象扫描注册的监听器列表,循环调用监听器的 operationComplete方法,将结果通知给监听器,监听器获取到结果之后,继续后续业务逻辑的执行,异步服务调用结束。

    需要指出的是,还有另外一种异步服务调用形式,就是不添加 Listener,用户连接发起 N次服务调用,然后依次从 RPC上下文中获取 Future对象,最终再主动 get结果,业务线程阻塞,相比于老的同步服务调用,它的阻塞时间更短,其工作原理如图1-4:

                              图1-4   异步服务调用主动get结果原理图

    异步服务调用的代码实例如下:

    xxxService1.xxxMethod(Req);
    Future f1 = RpcContext.getContext().getFuture();
    xxxService2.xxxMethod(Req);
    Future f2 = RpcContext.getContex().getFuture();
    Object xxResult1 = f1.get(3000);
    Object xxResult2 = f2.get(3000);

    假如 xxxService1 和 xxxService2 发布成异步服务,则调用 xxxMethod方法之后当前业务线程不阻塞,立即返回null,用户不能直接使用它的返回值,而是通过当前线程上下文RpcContext获取异步操作结果Future。获取到Future之后继续发起其他异步服务调用,然后获取另一个Future....最后,通过Future的get方法集中获取结果。无论是多少Future,采用此种方法用户线程最长阻塞时间为耗时最长的Future,即T = Max(t(future*));如果是同步调用,用户线程阻塞时间T = t(future1) + t(future2)+... ...+t(futureN)。异步服务调用相比于同步服务调用有两个优点
    【1】化串行为并行,提升服务调用效率,减少业务线程阻塞时间。
    【2】化同步为异步,避免业务线程阻塞。

    异步服务调用效果如图1-5所示:
     
                                                              图1-5  异步服务调用场景

    采用异步服务调用模式,最后调用三个服务异步操作结果 Future的 get方法同步等待应答,他的总执行时间T=Max(T1,T2,T3),相对于同步服务调用,性能提升效果非常明显。第二种基于 Future-Listener的纯异步服务调用,它的代码示例如下:

    xxxService1.xxxMethod(Req);
    Future f1 = RpcContext.getContext().getFuture();
    Listener l = new xxxListener();
    f1.addListener(1);
    ......后续代码省略

    基于 Future-Listener的异步服务调用相比于 Future-get模式更好,但是实际使用中有一定的局限性。

    2.3、并行服务调用:在大多数业务应用中,服务总是被串行地调用和执行,例如A调用B服务,B服务调用C服务,最后形成一个串行服务调用链:A-->B服务-->C服务-->......。串行服务调用比较简单,但是一些业务场景中,需要采用并行服务调用来降低E2E的时延。
    【1】多个服务之间逻辑上不存在互相依赖关系,执行先后顺序没有严格的要求,逻辑上可以被并行执行。
    【2】长流程业务,调用多个业务,对时延比较敏感,其中有部分服务逻辑上无上下文关联,可以并行调用。

    并行服务调用的目标主要有两个
    【1】降低业务 E2E时延;
    【2】提升这个系统的吞吐量;

    要解决串行调用效率低的问题,有两个解决对策
    【1】异步服务调用;
    【2】并行服务调用;

    并行服务调用的原理:一次同时发起多个服务调用,先做流程的Fork,在利用Future等主动等待获取结果,进行结果汇聚Join。实现并行服务调用的集中技术方案:
    【1】JDK7 的 Fork/Join,可以实现子任务的并行执行和结果汇聚;
    【2】BPM 的 Paraller Gateway(并行网关);
    【3】批量串行服务调用;

    JDK7 的 Fork/Join 底层会开启多个线程来分解任务,在服务框架中使用会导致依赖线程上下文传递的变量丢失、线程膨胀不可控等问题,因此在并行服务调用时不适合使用 JDK 的 Fork/Join并行执行框架。BPM 流程引擎支持并行流程(子流程)调用,它的执行示意图如图1-6所示:
     
                              图1-6  BPM Parallerl GateWay 工作流程

    Paraller Gateway(并行网管)能在一个流程里用来对并发建模。在一个流程模型里引入并发最直接的网关就是并行网管(Parallel Gateway),它允许 Fork执行多个路径,或者Join多个执行的到达路径。并行网管的功能基于即将到达的和即将离开的流程顺序流。
    【1】Fork:所有即将离开的顺序流将将以并行方式,为每个顺序流程建立一个并发执行器。
    【2】Join:所有的并发执行达到并行网关,在网关里面等待直到每个来到的顺序流的执行到达,条件满足后流程继续通过合并网关。

    从技术上看,不同的 BPM流程引擎具体实现细节也不同,但大多数都支持:通过创建子线程的方式实现并行调用、通过批量调用的方式实现伪异步并行调用。对于服务框架而言,BPM Parallel Gateway的功能可以满足需求,但是为了并行服务调用引入BPM流程引擎显然是得不偿失,我们可以参考 Parallel Gateway的伪异步并行调用来实现服务框架的并行服务调用。下面对批量串行服务调用实现并行服务调用的原理进行说明,如图1-7所示:
     
                                                        图1-7  批量服务调用原理图

    【1】服务框架提供批量服务调用接口供消费者使用,他的定义样例如下:parallelService.invoke(serviceName[], methodName[],args[]);
    【2】平台的并行服务调用器创建并行 Future,缓存批量服务调用上下文信息;
    【3】并行服务调用器循环调用普通的 Invoker,通过循环的方式执行单个服务调用,获取到单个服务的Future之后设置到Parallel Future中;
    【4】返回 Parallel Future给消费者;
    【5】普通 Invoker调用通信框架的消息发送接口,发送远程服务调用;
    【6】服务端返回应答,通信框架对报文做反序列化,转换成业务对象更新Parallel Future的结果列表;
    【7】消费者调用 Parallel Future的get(timeout)方法,同步阻塞,等待所有结果都返回;
    【8】Parallel Future通过对结果集进行判断,看所有服务调用是否都已经完成(包括成功、失败和异常);
    【9】所有批量服务调用结果都已经返回,Notify消费者线程,消费者获取到结果列表,完成批量服务调用,流程继续执行;

    通过批量服务调用+Future机制,我们实现了并行服务调用,而且没有创建新的线程,用户不用担心依赖线程上下文的功能出异常。该方案唯一的缺点就是用户需要调用平台提供的并行服务调用接口,这个会导致API层面的依赖,对于努力构建零依赖的服务框架而言不是最优的选择。但是零依赖事实是不存在的,即便100%XML配置也是一种配置依赖,所以在设计过程中要能够识别并抓主要矛盾点,做到有所舍,否则设计工作将步履维艰。

    2.4、泛化调用:泛化调用通常包含两种模式:泛化引用和泛化实现。泛化引用主要用于客户端没有API接口及数据模型的场景,参数及返回值中的所有 POJO均用 Map表示,通常用于框架继承,比如实现一个通用的服务测试框架。泛化实现主要用于服务端没有API接口及数据模型的场景,参数及返回值中的所有POJO均用Map表示,通常用于框架集成,比如实现一个通用的远程服务Mock框架。泛化调用的设计要点如下:

    【1】分布式服务框架提供泛化接口,供服务提供者实现和消费者引用,它的参数定义如下:

    public interface GenService{
        Object invoke (String methodName, String[] paramTypes, Object[] args);
    }

    【2】消费者引用泛化接口,则直接将请求参数转换成Map,应答消息也自动转换成Map。
    【3】服务提供者如果使用泛化实现发布服务,则自动将请求参数转换成Map,调用GenService的泛化实现类,应答消息自动包装成Map返回。

    泛化调用由于比较灵活,没有服务契约,在实际项目中慎用,它通常用于测试集成、系统上线之后的回声测试等。

    总结:服务框架往往支持多种形式的调用,我们在设计服务调用时,需要充分考虑用户的使用习惯以及业务面临的主要挑战,在矛盾中做出平衡和取舍,这是一个优秀架构师的基本功。

    ----如果喜欢,点个  红心♡  支持以下,谢谢----

    展开全文
  • 显式调用与隐式调用

    2018-09-27 12:46:10
    前面课程已经多次演示了显式调用和隐式调用,本节我们集中对这两种调用方式进行讲解。 1、显式调用 我们通过Intent调用组件时,可以直接指定组件类,这种方式称为显式调用,下面给出一段代码,显式调用一个...

    前面课程已经多次演示了显式调用和隐式调用,本节我们集中对这两种调用方式进行讲解。

    1、显式调用

    我们通过Intent调用组件时,可以直接指定组件类,这种方式称为显式调用,下面给出一段代码,显式调用一个Activity

    Intent intent = new Intent(LoginActivity.this, HomeActivity.class);

    startActivity(intent);

    显式调用通常用于访问应用程序本身的组件,但也不尽其然,也就是说一个应用程序同样可以直接指定另一个应用程序的组件。下面先给出一个跨应用程序访问Activity的样例代码。

    Intent intent = new Intent();

    intent.setClassName("com.practise.salary.hello","com.practise.salary.hello.ViewCallActivity");

    startActivity(intent);

           注意:ViewCallActivity必须声明属性android:exported="true"(该属性将在后面的课程详细介绍),如下图所示:

    在Android中,包名称唯一标示一个应用,所以我们可以在Intent中指定要调用的包名称和类名称来启动其他应用中的组件。除了指定包名名外,我们还可以通过createPackageContext来创建包名对应的Context,完成对其他应用程序组件的调用。

    代码如下:

    try {

        Intent intent = new Intent();

        Context viewContext = createPackageContext("com.practise.salary.hello", Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);

        intent.setClassName(viewContext, "com.practise.salary.hello.ViewCallActivity");

        startActivity(intent);

    }catch (PackageManager.NameNotFoundException exp){

        Log.v(this.getClass().getName(), "异常", exp);

    }

    createPackageContext方法定义在ContextWrapper中,ContextWrapper是Context的一个子类。Activity与ContextWrapper之间的关系如下图所示:

    createPackageContext的方法签名如下所示:

    Context createPackageContext (String packageName, int flags)

    其中:

    1. packageName:包名,要得到Context的包名
    2. flags:标志位,有CONTEXT_INCLUDE_CODE和CONTEXT_IGNORE_SECURITY两个选项。CONTEXT_INCLUDE_CODE的意思是包括代码,也就是说可以执行这个包里面的代码。CONTEXT_IGNORE_SECURITY的意思是忽略安全警告,如果不加这个标志的话,有些功能是用不了的,会出现安全警告。

    除了上述两种外,我们观察CompomentName构造函数,会发现,也可以通过ComponentName对象实现对其他应用程序组件的调用

    Intent intent = new Intent();

    intent.setComponent(new ComponentName("com.practise.salary.hello", "com.practise.salary.hello.FirstActivity"));

    startActivity(intent);

    2、隐式调用

    我们可以通过指定Action,Category,Data,由系统根据条件,过滤选择满足条件的组件进行调用,这种调用方式称为隐式调用,下面给出隐式调用Activity的代码。

    Intent intent = new Intent();

    intent.setAction("com.practise.salary.hello.CALL");

    intent.addCategory("com.practise.salary.hell.VIEW");

    startActivity(intent);

           我们可以对某个Activity配置过滤条件,系统会调用对应的Activity。配置如下图所示:

    我们通常通过隐式调用,调用系统Activity,例如下面代码,调用发送短信的Activity。

    Intent intent = new Intent();

    intent.setAction(Intent.ACTION_SENDTO);

    intent.setData(Uri.parse("smsto:10086"));

    intent.putExtra("sms_body","The SMS text");

    startActivity(intent);

    关于系统调用,后面会有专门的章节进行讲解,此处只是做一个介绍。

    (张伟:2018年9月27日)

    (转载时请注明来源)

    展开全文
  • 实验3 系统调用

    2017-12-15 21:09:26
    系统调用 实验目的 建立对系统调用接口的深入认识 掌握系统调用的基本过程 能完成系统调用的全面控制 为后续实验做准备 实验内容 此次实验的基本内容是:在Linux 0.11上添加两个系统调用,并编写两个简单的应用...
  • 此接口调用与被调用,都是在springMVC框架下使用 参数以json格式传输。 别人调用我们的接口,与controller方法开发类似 @RequestMapping("/otherUseMe.do") public void otherUseMe ...
  • 方法的定义及调用

    2018-07-27 08:44:23
    2、定义方法就是编写有特定功能的一段代码,在程序中使用同样功能的地方可以调用定义好的方法,实现代码的重用 方法声明或称为定义方法 语法格式: [访问控制符] [修饰符] 返回值类型 方法名 (参数类型 形式参数...
  • 请求响应流程 设置连接参数的方法 setAllowUserInteractionsetDoInputsetDoOutputsetIfModifiedSincesetUseCachessetDefaultAllowUserInteractionsetDefaultUseCaches ...设置请求头或响应头 ...
  • 目录 1、标准输入输函数 2、格式控制字符 2.1、U格式控制字符与f格式控制字符 2.2、C格式控制字符与S格式控制字符 2.3、O格式控制字符与X格式控制字符 3、字符输出函数 4、字符串的输入输出 ...int main(v...
  • 最近我也在翻看一些源代码,从头到尾的看了一下HashMap的底层发现了一个这个东西——安全失败机制(fail-safe),以此作为记录! 首先,fail-fast 机制是java集合(Collection)中的一种错误机制。...
  • 这些函数及调用函数的行为统称为系统调用(system call),也就是应用对操作系统(system)的功能进行调用(call)的意思。在前面的程序中用到了time()及printf()等函数,这些函数内部也都使用了系统调用。这里之...
  • 首先jvm会在内存中开辟一块存储空间做为线程栈空间,...方法调用时,调用栈不断处于涨落之中,如果调用的层级过深,调用栈可能会溢出 Spring创建的bean默认是singleton的,线程调用某个bean时,是持有一个bean对象...
  • JAVA类之间方法的调用

    2018-11-03 11:33:41
    JAVA类方法的调用一、静态方法调用其他方法:1. 静态方法调用非静态方法2.静态方法调用静态方法二、非静态方法调用其他方法1.非静态方法在同一类内调用其他方法2.非静态方法在不同类之间调用其他方法 注:调用方法...
  • 说起函数调用,相信大家也不会陌生,然而对于初学Shell的我来说,Shell中函数调用方式却有点让我不太习惯,自己也走了不少的弯路,因为传递参数时出了一个很“自然”的错误,也让我吃了不少的苦头,所以总结一下...
  • Eclipse就会显示出这个方法被哪些方法调用,最终产生一个调用关系树。 2.(次推)选中该方法,Ctrl+Shift+G 就显示这个方法被谁引用了 ,  查询在整个工作区中被谁引用了 3.选中该方法,Ctrl + H(全
  • 2.其中的参数self代表类的实例,在调用方法时由系统自动提供 3.方法定义时必须指明self参数   类的方法的调用   与普通的函数调用类似   1.类的内部调用:self.方法名>(参数列表)。 2.在类的外部调用:...
  • 在LabVIEW程序设计中,调用VI一般分为静态调用和动态调用,静态调用是指被调用VI在VI调用程序的程序框图上直接放置子VI,它与VI调用程序同时加载,而且常驻内存 ,直至调用它的VI运行结束。动态调用VI是指在调用程序...
  • js系列教程4-函数、函数参数教程全解js中的4种函数调用模式javascript一共有4种调用模式:函数调用模式、方法调用模式、构造器调用模式和间接调用模式。【1】函数调用模式 当一个函数并非一个对象的属性时,那么它...
  • Python是解释性语言, 底层就是用c实现的, 所以用python调用C是很容易的, 下面就总结一下各种调用的方法, 给出例子, 所有例子都在ubuntu9.10, python2.6下试过.   1. Python 调用 C (base) 想...
  • 调用函数: A.py文件: def add(x,y): print('和为:%d'%(x+y)) B.py文件: import A A.add(1,2) 或 from A import add add(1,2) 调用类: A.py文件: class A: def __init__(self,xx,yy): self.x=
1 2 3 4 5 ... 20
收藏数 7,090,421
精华内容 2,836,168
关键字:

调用