编程 - 回调函数、共享内存概念与使用,以及进程间函数调用的讨论

本文探讨了回调函数的概念与使用,以及如何通过共享内存实现进程间的数据同步。介绍了回调函数的定义、使用方法,并详细讲解了共享内存的工作原理及其在进程间通信的应用。

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

回调函数、共享内存概念与使用,以及进程间函数调用的讨论

    最近在做项目时,需要调用一个第三方的库和框架,在项目进行一段时间之后,突然发现对该第三方的库以及框架的实现机制不太了解,现根据其函数声明以及使用方式推导其实现机制。会有出入。

1、问题描述

    该库和框架是用于仿真的,为非标库;同时提供了一个框架用于实现对库的使用,这样用户只需要在框架内写一些代码就可以实现相关的功能,这个软件的基本功能基本描述如下:

      1、一台“服务器”,同时有多个“客户端”;之所以加引号是因为不是严格意义上的服务器-客户端的概念,只是逻辑上功能一致;

      2、每个客户端采用相同的框架,并调用相同的库;

      3、为了保证运行时客户端之间数据传递的同步性,由服务器向每个客户端发送同样的数据,该数据包含了一些参数,通过这些参数,客户端之间保持同步运行并实现各自的操作;

    通过以上描述,最先想起来的是利用Socket,通过Socket(UDP)连接,每个客户端连接到服务器,再由服务器向每个客户端发送数据,也可以实现以上的功能;暂不讨论采用Socket方法的优劣。在这里采用另外一种实现方式:回调函数。回调函数的概念网上有一大堆,关于它的理解,在下面的章节我选取了一些比较好理解的进行展示(如有侵权,请及时联系)。

    结合该问题,通过具体实施,有助于加强对回调函数等的了解。

 

2、回调函数

2.1、回调函数的概念    

关于回调函数的概念,网上有很多解释,但大多都很抽象。其实回调函数的概念也很简单,我的理解是:

      1、回调函数,Call Back Function,意如其名。可以简单的理解为A调用了B中的函数,而在B中的这个函数中又调用了A中的函数,这样就形成了一个逻辑上的“回调”机制;

      2、关于A和B的理解,可以理解为一个程序的两个部分;也可以理解为一个程序、一个库(静态or动态);当然最有助于理解的还是把A和B理解成两个程序,但是这样只是有助于理解,在实际实现过程中会存在很多的麻烦,接下来的章节会进行阐述;

      3、可以把回调函数理解成两层意思:一是回调函数的这个过程,是动态的;二是回调的那个函数,是静态的;

2.2、回调函数的定义

    下面通过一个简单的例子来展示回调函数的使用:

 

#include <stdio.h>
void PrintWelcome(int n)    //被回调的函数
{
    printf("%d is coming!", n);
}

void CallBack(int nC, void(*print)(int))    //调用“被回调的函数”的函数,简称回调函数
{
    int nT = nC;
    print(nT);
}

void main()
{
    CallBack(3, PrintWelcome);     //调用回调函数,其中一个参数为“被调用的函数”的函数名
    return;
}

    由上面的代码可以看出:

    1)、main函数调用了CallBack函数;

    2)、由于main函数在调用CallBack时传递了PrintWelcome函数的函数指针,所以CallBack函数内部的print函数实际上就是在调用PrintWelcome函数;

    关于回调函数的使用,需要注意一下几点:

    1)、被回调的函数的定义方式:该函数的定义没有什么特别需要注意的地方,按照正常方式定义函数即可;另外需要注意的是,任何一个定义过的函数在内存中都会分配一块内存用于存储该函数的代码,存储形式为汇编语言的形式,而这块内存的首地址即为该函数名,即,函数名代表了该函数存储的内存地址的首地址

    2)、回调函数的定义:在定义回调函数时,必须保证该函数的一个参数为函数指针的形式,并且该函数指针代表的函数形式需和“被回调的函数”的函数形式保持一致!即,函数的返回值类型保持一致,函数参数列表保持一致

        为了解决函数形式很复杂的情况下,代码书写的问题,可以采用typedef将函数形式重定义成一个容易记的变量名,参考如下形式:

 

typedef void (*PRINT)(int nC);

       当采用上述方式表达之后,回调函数就可以直接写成:

 

 

void CallBack(int nC, PRINT print)
{
    int nT = nC;
    print(nT);
}

      可以明显看出这样写代码会更加简洁,并且不容易写错。

 

    3)、在调用回调函数时,需要保证传递参数必须为“被回调函数”的函数名!

2.3、使用回调函数

    好了,回调函数的概念以及怎么使用搞清楚了,下面开始具体使用了,看第一章节的问题描述,开始使用回调函数设计程序。

    服务器是一个独立的程序,各个客户端分别是一个独立的程序,现在服务器要回调各个客户端程序中的函数。emmmmm.....问题来了,

    1)、两个独立的程序、不同的进程,服务器该怎么知道客户端中定义的函数呢?

      可以采用同一个函数定义的形式,双方都采用统一各头文件或者引用同一个库,这样大家都能识别同一种形式的函数定义了;

    2)、服务器端要调用回调函数,就需要知道“被回调函数”的函数名,那服务端怎么知道客户端中定义的“被回调的函数”的函数名呢?

      需要知道的是,这个“函数名”是有具体含义的,它不仅仅代表了函数的名称,也代表了函数定义存储在内存块上的首地址,而且在调用回调函数时,传递的参数实际上是地址

    3)、哦~那看来需要传递一个地址,那么我该怎么在程序间传递地址呢?

      Socket不行,它只能传递字符串;

      利用配置文件?不行,这样虽然能获取一个地址,但没有任何意义;

      诶,可以这样,我可以制作一个库,在这个库里,提供两个函数,一个用于得到“被回调函数”的地址,即set;一个用于获得“被回调函数”的地址,即get。然后服务器和客户端同时调用这一个库,一个set,一个get这样不接可以传递了吗!

    4)、需要注意的是,虽然两个程序都同时调用了同一个库,但是,这两个程序是分别把库加载进了进程申请的空间中的,虽然调用了同一个库,但是,这两个库之间是八竿子打不着的关系,更别提,数据传递了,这种方法不行,那有没有一种大家都认识的内存地址控件呢?

     还真有,那就是采用共享内存的方式。共享内存实际上是一个内核对象,内核对象大家都能识别,这样应该就能传递了吧。

 

3、共享内存

3.1、共享内存的概念

    共享内存又称为内存映射文件。共享内存从字面意义解释就是多个进程可以把一段内存映射到自己的进程空间,以此来实现数据的共享以及传输。意思就是,在计算机上申请了一块内存,该内存对所有的进程都可见,并且所有的进程都可以去访问它。需要注意的是,各进程是把这块内存地址映射到了自己的进程空间里,因为每一个打开的进程都有自己的进程空间,并不是在进程里都去操作那块地址,在进程里操作的是它的映射地址。

    拿Windows来说,Windows提供了多种机制来实现进程间的通信,比如最常见的Socket,还有RPC、COM、消息机制、邮件槽、管道等机制;但是,这些机制归根结底都会用到内存映射文件,即共享内存。

3.2、共享内存的使用

    要使用共享内存,需要用到几个内核函数:

     1)、CreateFileMapping:告诉系统映射对象需要多大的物理存储空间,声明如下:

 

HANDLE CreateFileMapping(
    HANDLE hFile,             //hFile是需要映射到进程地址空间的文件的句柄。如果没有有效的句柄,可以用INVALID_HANDLE_VALUE
    PSECURITY_ATTRIBUTES psa, //psa用于文件映射内核对象,一般传NULL(提供了默认的安全性,此外表明返回的句柄不可继承)
    DWORD fdwProtect,         //指定给物理存储器页面的保护属性,一般传PAGE_READWRITE即可,表示可读取数据并能够写入数据
    DWORD dwMaximumSizeHigh,  //用于告诉系统内存映射文件的最大大小,以字节为单位,该参数表示的是高32位,对于<4GB的数据来说,该参数始终为0
    DWORD dwMaximumSizeLow,   //该参数表示的是低32位,如果需要用当前的文件大小创建一个文件映射对象,两个参数都为0即可。
    PCTSTR pszName            //表示一个以'\0'结束的字符串,用来给文件映射对象指定一个名称。可以传NULL,也可以赋一个具有标识的字符串
);

 

     2)、MapViewOfFile:将数据映射到进程的地址空间,声明如下:

 

PVOID MapViewOfFile(
    HANDLE hFileMappingObject,  //文件映射对象的句柄,是之前调用CreateFileMapping函数时返回的
    DWORD dwDesireAccess,       //dwDesireAccess表示想要如何访问数据,可以指定为FILE_MAP_ALL_ACCESS(READ/WRITE/COPY)
    DWORD dwFileOffsetHigh,     //用于告诉系统应该把数据文件中的哪个字节映射到视图中的第一个字节。文件的偏移量必须是系统分配粒度的整数倍
    DWORD dwFileOffsetLow,      //这两个参数都可以指定为0
    SIZE_T dwNumberOfBytesToMap //用来指定需要映射到地址空间的数据的大小
);

    下面通过两个例子用于展示共享内存的使用;

 

    ProcessA:

 

#define DATA_SIZE  256
const wchar_t mapFileName[] = L"myFirstMapping";
HANDLE myFileMappingHandle = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,DATA_SIZE,mapFileName);
if(myFileMappingHandle==NULL)
{
    printf("创建文件映射内核对象失败!");
    CloseHandle(myFileMappingHandle);
    return 0;
}
char* pBuf = (char *)MapViewOfView(myFileMappingHandle,FILE_MAP_ALL_ACCESS,0,0,DATA_SIZE);
while(1)
{
    cout << "input..."  << endl;
    getchar();
    char szInfo[DATA_SIZE] = {0};
    gets_s(szInfo);
    strncpy(pBuf, szInfo, DATA_SIZE-1);
    pBuf[DATA_SIZE-1] = '\0';
}
UnmapViewOfFile(pICBR);
CloseHandle(myFileMappingHandle);
return 0;

    ProcessB:

 

#define DATA_SIZE  256
const wchar_t mapFileName[] = L"myFirstMapping";
HANDLE myFileMappingHandle = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,DATA_SIZE,mapFileName);
if(myFileMappingHandle==NULL)
{
    printf("创建文件映射内核对象失败!");
    CloseHandle(myFileMappingHandle);
    return 0;
}
char* pBuf = (char *)MapViewOfView(myFileMappingHandle,FILE_MAP_ALL_ACCESS,0,0,DATA_SIZE);
while(1)
{
    cout << "output..."  << endl;
    getchar();
    cout << pBuf << endl;
}
UnmapViewOfFile(pICBR);
CloseHandle(myFileMappingHandle);
return 0;

 

 


 

 

 

 

3.3、使用共享内存

    嗯,共享内存已经理解了,下面开始使用共享内存解决前面提出的问题。

    我在ProcessA里面定义了一个函数ICBR,调用set函数得到函数指针,并通过get函数获得函数指针并赋给内存变量(char* pBuf类型的变量),但是当我在ProcessB中获取内存变量的值时发现,获得地址并不正确。而且始终是同一个值。这是为什么呢?

    因为虽然两个进程都使用了共享内存,但是就向前面所说的一样,每一个进程在启动时,系统都会给进程分配一块虚拟的内存空间,不同进程的进程空间是独立的,而共享内存的使用仅仅是映射到进程空间,即在进程中得到的地址和实际上共享内存中存贮的地址是不一致的,它反映的仅仅是该进程空间中的内存地址,还是虚拟的。

    那还有什么方式能够实现吗?

4、其实现的可能方式

    通过以上的分析,猜测其实现的可能方式如下:

     由上图所示,可能采用的方式是:

     1)、服务器端与客户端都加载了相同的库,在库中定义了Socket连接,以及回调函数的定义;

     2)、服务器端通过库发送了一系列数据:包括消息类型以及参数列表;消息类型用于表明该消息适用于回调函数的,参数列表用于表明向回调函数传递的参数;

     3)、当客户端的库收到了Socket消息之后,就调用回调函数函数,同时传参;

     4)、客户端的“被回调的函数”执行相关操作。

    之所以采用这种方式,而不采用直接用Socket的连接的方式,是因为,它需要把一部分操作给封装起来,同时,还需要向用户暴露一部分的操作,这样用户就能根据指定的框架和指定的参数来执行一些定制化的操作。

5、进程间回调函数的调用

    在了解进程间回调函数的使用时,除了可以采用上述的间接的方式实现回调函数的间接调用外,好像还可以用COM接口实现同样的功能,未验证,不多做阐述。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值