新闻  |   论坛  |   博客  |   在线研讨会
动态库中的函数实现互斥调用
电子禅石 | 2021-12-09 20:27:14    阅读:8806   发布文章

(1条消息) 动态库中的函数实现互斥调用_卢平光的博客-CSDN博客_函数互斥


一直在纠结一个问题:


如果一个函数使用互斥锁可以防止被调用时重入的情况,但是如果该函数以so的形式提供给使用者(其它进程),那么如何做到各进程间对于该函数的互斥调用呢?


首先明确下前提:


so被进程加载时,代码段共享,但是所有变量(局部、全局、静态变量)都是各进程copy一份私有使用。

也就是说,想要在so内实现一个不可重入的函数还是比较困难的,因为所有变量都是独立的,但是考虑如下场景:驱动层给了一个视频码流录制的接口,并且没有在驱动层做互斥,但实际上这个接口同一时间只可能被一个进程调用,那么很明显,串接到so中的接口必须实现该接口的原子调用。


解决思路:


由so创建一块共享内存,放置一份进程共享的互斥锁

第一步的动作需要寻找一个合适点自动完成,比如so加载时

各进程在调用so接口时,接口内部使用该共享锁完成互斥调用

————————————————

1、互斥锁共享

so中的互斥锁属于私有数据,加载该so的进程都会拷贝一份,那么就无法在进程间共享使用该锁。所以必须将锁放到进程间的共享内存中,确保锁只有一把。首先定义一个锁结构体

// 定义进程锁结构体
typedef struct Mutex_Info {
    // 锁以及状态
    pthread_mutex_t lock;
    pthread_mutexattr_t lock_attr;
    // 在共享内存中的标识符
    int FLAG;
} mutex_info_t;

要想把锁放到共享内存,那么先创建一块内存

/**
 *  返回一片共享内存标识符,用于后续获取该共享内存,以及销毁该共享内存
 *  INDEX_OF_KEY —— 自定义的该共享内存序号
 *  LENGTH —— 共享内存大小
 */
const int create_sharemem(const int INDEX_OF_KEY, const unsigned int LENGTH) {
    // 生成key
    const char* FILE_PATH = "./";
    key_t key = ftok(FILE_PATH, INDEX_OF_KEY);

    // 创建共享内存空间 
    //多个进程调用该函数,只要确保KEY相同,那么只会创建同一块内存
    const int FLAG = shmget(key, LENGTH, IPC_CREAT | 0666);
    return FLAG;
}

其中的init函数是锁的初始化,注意需要设置其进程间共享属性

const int init_mutex(void* pthis) {
    mutex_info_t* mp = (mutex_info_t*)pthis;
    // 初始化锁状态,设置状态状态为——进程共享
    pthread_mutexattr_init(&(mp->lock_attr));
    pthread_mutexattr_setpshared(&(mp->lock_attr), PTHREAD_PROCESS_SHARED);
    // 用锁状态来初始化锁
    pthread_mutex_init(&(mp->lock), &(mp->lock_attr));
    return 0;
}

有了以上两个函数,其实就可以写一个so被加载时自动执行的初始化函数,这样可以保证so的使用者不必关心内存、锁的创建、销毁和使用。所以先插讲下so加载时自动执行函数的方法。


2、so加载时自动执行函数



GNU C 的一大特色就是__attribute__ 机制。attribute 是一个编译指令,可以设置函数属性(Function Attribute )、变量属性(Variable Attribute )和类型属性(Type Attribute )。

attribute 书写特征是:attribute 前后都有两个下划线,并切后面会紧跟一对原括弧,括弧里面是相应的__attribute__ 参数。

若将某一函数的声明中添加 __ attribute__((constructor)) 属性,那么它具有两种运行时机


若函数所在源文件被编译为可执行文件,那么该函数可以在main函数执行前被调用

————————————————

#include <stdio.h>
#include <stdlib.h>
static int * g_count = NULL;
__attribute__((constructor)) void load_file()
{
    printf("Constructor is called.\n");
}
__attribute__((destructor)) void unload_file()
{
    printf("destructor is called.\n");
}

int main()
{
   printf ("this is main function\n");
    return 0;
}

 注意正常退出才会自动执行destructor 的修饰的函数。


  • 若函数所在源文件被编译为共享库,那么该函数可以在共享库被其它进程显式dlopen或者隐式由操作系统加载时都会优先执行

  • //process source file
    #include <stdio.h>
    #include <string.h>
    #include <dlfcn.h>
    
    int main(int argc, char **argv)
    {
    	void (*print)();
    	int (*add)(int, int);
    	void *handle;
    	
    	if (argc < 2)
    		return -1;
    	
    	handle = dlopen(argv[1], RTLD_LAZY);
    	if (!handle) {
    		printf("dlopen failed: %s\n", dlerror());
    		return -1;
    	}
    	
    	print = dlsym(handle, "print");
    	if (!print) {
    		printf("dlsym failed: %s\n", dlerror());
    		return -1;
    	}
    	print();
    	
    	add = dlsym(handle, "add");
    	if (!add) {
    		printf("dlsym failed: %s\n", dlerror());
    		return -1;
    	}
    	add(1, 2);
    	
    	dlclose(handle);
    	
    	return 0;
    }
    
    
    //libss.so source file
    
    #include <stdio.h>
    #include <string.h>
    
    void print() 
    {
    	printf("I am print\n");
    }
    
    int add(int a, int b)
    {
    	printf("Sum %d and %d is %d\n", a, b, a + b);
    	return 0;
    }
    //static void king() __attribute__((constructor(101))); the following is also right
    static __attribute__((constructor(101))) void king()
    {
    	printf("I am king\n");
    }


gcc -Wall -shared -fPIC -o libss.so libss.c -ldl

gcc -Wall -o udlopen udlopen.c

./udlopen libss.so

I am king
I am print
Sum 1 and 2 is 3

__ attribute__((constructor))的这一特性可以被用在许多场合。比如某个so的功能实现需要预先映射一块共享内存,如果要求所有使用该so的进程在加载时手动去做这一步骤是非常不合理的,此时可以将内存映射实现放在__ attribute__((constructor))属性声明的函数中,这样每次so被加载就会自动完成共享内存的创建映射,对so的使用者完全透明,确实够巧妙!

————————————————

3、so加载时自动创建共享内存与互斥锁

//全局变量
mutex_info_t tPtr = NULL;
__ attribute__((constructor)) static void pre_init(void)
{
      int flag = create_sharemem(127, sizeof(mutex_info_t ));
      //NULL参数代表由操作系统选择共享内存中的合适位置返回给内存申请者,测试发现由于共享内存一共就只有mutex_info_t 大小,
      //所以每次返回的地址相同 这也能保证so被多个进程加载使用的是同一把锁
      tPtr = (mutex_info_t *)shmat(FLAG, NULL, SHM_R | SHM_W);
      //内存内结构体的FLAG如果不是共享内存的索引,那么表示是第一次申请内存,需要对锁初始化
      //避免多次加载so时多次init锁
      if(tPtr->FLAG != flag)
      {
		 	tPtr->FLAG == flag;
		 	init_mutex(tPtr);
		 	printf("first make mem, init lock");
	   }
	   else
	   {
			printf("mem, lock has been init");
	    }
}

__ attribute__((destructor)) static void late_destory(void)
{
	  struct shmid_ds shminfo;
      //共享内存有引用计数,所以多次写在so调用释放共享内存时,只有最后计数为0时才会真正释放
	  shmctl(tPtr->FLAG, IPC_RMID,NULL);
      shmctl(tPtr->FLAG, IPC_STAT,&shminfo);
      //虽然tPtr不能被进程共享,但是每个进程的SO呗加载时都会重新更新tPtr的值,所以可放心使用
      //当内存引用计数变为0时(实际测试是1),代表so不被使用,可销毁锁
      if(shminfo.shm_nattch == 1)
      {
            pthread_mutex_destory(&(tPtr->lock));
            pthread_mutexattr_destory(&(tPtr->lock_attr));
      }
}

有了保护机制,可以再写一个接口,接口假设不可重入:

void Asyncprint()
{
	pthread_mutex_lock(&(mp->lock));
	printf("now you can call me");
	sleep(10);
	printf("all me finish!");
	pthread_mutex_unlock(&(mp->lock));
}

以上函数模拟的场景是:Asyncprint执行一次需要10s,中间不允许重入,so编译方法:

gcc lock.c -fPIC -shared -o liblock.so

可以同时跑两个相同进程调用该so的接口,看看接口是否互斥调用

//main.c
//gcc main.c -L. -llock -lpthread -o 1.exe
//gcc main.c -L. -llock -lpthread -o 2.exe
//可同时使用ipcs -m c查看共享内存信息
#include <stdio.h>
#include "lock.h"
int main()
{
   Asyncprint();
}

同时运行1.exe 2.exe看打印:

在这里插入图片描述

完整源码:

//main.c 
//gcc lock.c -fPIC -shared -o liblock.so    编译动态库
//gcc main.c -L.  -llock  -lpthread -o 1.out 编译可执行程序1
//gcc main.c -L.  -llock  -lpthread -o 2.out 编译可执行程序2
//ipcs -m shell中查看当前shm情况
#include <stdio.h>
#include <sys/shm.h>
#include "lock.h"
int main() {

    //方式1 可执行程序自己调用函数分配共享内存,创建互斥锁
	//此时需删除lock.c中的constructor与desstructor函数
    /*mutex_info_t * mp = create_mutex_package(111);
    shmctl(557060, IPC_RMID, NULL)
    asyncprint();
    destory_mutex_package(mp); 
	*/
	//方式2 内存与互斥锁均由so加载时,其自己的constructor与desstructor函数负责创建与销毁
	asyncprint();

	return 0;

}


//lock.h
#ifndef __LOCK_H_
#define __LOCK_H_

#include <pthread.h>


typedef struct MUTEX_PACKAGE {

    pthread_mutex_t lock;
    pthread_mutexattr_t lock_attr;

    int FLAG;
} mutex_info_t ;

extern const void asyncprint();
#endif


lock.c

//gcc lock.c -fPIC -shared -o liblock.so

#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <assert.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#include "lock.h"

mutex_info_t * mp = NULL;
int FLAG =0;
/**
 *  返回一片共享内存标识符,用于后续获取该共享内存,以及销毁该共享内存
 *  INDEX_OF_KEY —— 自定义的该共享内存序号
 *  LENGTH —— 共享内存大小
 */
const int create_sharemem(const int INDEX_OF_KEY, const unsigned int LENGTH) {
    // 生成key
    const char* FILE_PATH = "./";
    key_t key = ftok(FILE_PATH, INDEX_OF_KEY);

    // 创建共享内存空间
    const int FLAG = shmget(key, LENGTH, IPC_CREAT | 0666);

    return FLAG;
}



// 初始化进程锁结构体
const int init_mutex(void* pthis) {
    mutex_info_t * mp = (mutex_info_t *)pthis;
    // 初始化锁状态,设置状态状态为——进程共享
    pthread_mutexattr_init(&(mp->lock_attr));
    pthread_mutexattr_setpshared(&(mp->lock_attr), PTHREAD_PROCESS_SHARED);
    // 用锁状态来初始化锁
    pthread_mutex_init(&(mp->lock), &(mp->lock_attr));

    return 0;
}


__ attribute__((constructor)) static void pre_init(void)
{
      int flag = create_sharemem(127, sizeof(mutex_info_t ));
      //NULL参数代表由操作系统选择共享内存中的合适位置返回给内存申请者,测试发现由于共享内存一共就只有mutex_info_t 大小,
      //所以每次返回的地址相同 这也能保证so被多个进程加载使用的是同一把锁
      tPtr = (mutex_info_t *)shmat(FLAG, NULL, SHM_R | SHM_W);
      //内存内结构体的FLAG如果不是共享内存的索引,那么表示是第一次申请内存,需要对锁初始化
      //避免多次加载so时多次init锁
      if(tPtr->FLAG != flag)
      {
		 	tPtr->FLAG == flag;
		 	init_mutex(tPtr);
		 	printf("first make mem, init lock");
	   }
	   else
	   {
			printf("mem, lock has been init");
	    }
}

__ attribute__((destructor)) static void late_destory(void)
{
	  struct shmid_ds shminfo;
      //共享内存有引用计数,所以多次写在so调用释放共享内存时,只有最后计数为0时才会真正释放
	  shmctl(tPtr->FLAG, IPC_RMID,NULL);
      shmctl(tPtr->FLAG, IPC_STAT,&shminfo);
      //虽然tPtr不能被进程共享,但是每个进程的SO呗加载时都会重新更新tPtr的值,所以可放心使用
      //当内存引用计数变为0时(实际测试是1),代表so不被使用,可销毁锁
      if(shminfo.shm_nattch == 1)
      {
            pthread_mutex_destory(&(tPtr->lock));
            pthread_mutexattr_destory(&(tPtr->lock_attr));
      }
}



原文链接:https://blog.csdn.net/ludashei2/article/details/115061988


*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。

参与讨论
登录后参与讨论
电子禅石  2022-03-16 17:22:20 

[http://www.blogjava.net/baicker/archive/2008/05/09/199491.html] 在 Linux 上,GCC 有一个扩展,允许指定当可执行文件或者包含它的共享对象启动或停止时应该调用某个函数。语法是 __attribute__((constructor)) 或 __attribute__((destructor))。这些基本上与构造函数及析构函数相同,可以替代 glibc 库中的 _init 和 _fini 函数。 这些函数的 C 原型是: void __attribute__ ((constructor)) app_init(void); void __attribute__ ((destructor)) app_fini(void);

属于自己的技术积累分享,成为嵌入式系统研发高手。
推荐文章
最近访客