结论:
- so被进程加载时,代码段共享,但是所有变量(局部、全局、静态变量)都是各进程copy一份私有使用。
一、动态库中的全局变量测试(包括static全局变量)
新建一个动态库,代码如下
#include <stdio.h>
#include "my_lib.h"
int g_val = 200;
//测试库中的全局变量是否独立或共享
static void func_1();
void func(int i)
{
func_1();
g_val = i;
printf("===== g_val= %d\n", g_val);
}
static void func_1()
{
printf("===== g_val= %d\n", g_val);
}
测试程序如下:test1.c
#include <stdio.h>
#include "my_lib.h"
int main()
{
func(10);
while(1)
{
sleep(5);
}
}
test2.c
#include <stdio.h>
#include "my_lib.h"
int main()
{
func(30);
while(1)
{
sleep(5);
}
}
执行结果如下:
test1:
===== g_val= 200
===== g_val= 10
test2:
===== g_val= 200
===== g_val= 30
结论:在共享库中的全局变量是基于进程独立的。我们知道每一个进程空间都拥有自己的进程空间。(将全局变量修改为static也是一样的运行结果)
进程空间组成如下:
程序段(Text):程序代码在内存中的映射,存放函数体的二进制代码。
初始化过的数据(Data):在程序运行初已经对变量进行初始化的数据。
未初始化过的数据(BSS):在程序运行初未对变量进行初始化的数据。
栈 (Stack):存储局部、临时变量,函数调用时,存储函数的返回指针,用于控制函数的调用和返回。在程序块开始时自动分配内存,结束时自动释放内存,其操作方式类似于数据结构中的栈。
堆 (Heap):存储动态内存分配,需要程序员手工分配,手工释放.注意它与数据结构中的堆是两回事,分配方式类似于链表。
在应用程序编译的时候,不同的应用程序引用同一个库,那么动态库的代码是共享的。只有一份,但是数据却是每一个进程空间一份拷贝。(这里的数据包括库中的全局变量和静态变量)。所以一个应用程序修改库中的全局变量也不会影响到另一个库的引用。因为数据是独立的,代码是共享的。
二、动态库中使用进程间同步互斥机制
当我们在动态库中使用一些进程间同步互斥的代码的时候,调用这些库的app之间是否会相互受到影响呢?(线程间同步互斥的机制肯定没有影响)。这里采用文件锁来测试
动态库代码如下:
#include <unistd.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include "my_lib.h"
static int g_val = 200;
//测试库中的进程同步互斥机制是否有影响
void func(int i)
{
FILE *f = fopen("test_1.c", "r");
if(0 == flock(fileno(f), LOCK_EX))
{
printf("==================\n");
sleep(100);
fclose(f);
flock(fileno(f), LOCK_UN);
}
}
测试代码test1.c
#include <stdio.h>
#include "my_lib.h"
int main()
{
func(10);
while(1)
{
sleep(5);
}
}
测试代码test2.c
#include <stdio.h>
#include "my_lib.h"
int main()
{
func(10);
while(1)
{
sleep(5);
}
}
结论:用于动态库中使用了进程间同步互斥代码,所以调用该库的app相互之间是有影响的
总结:全局变量 静态变量在库中 可以随便用,进程间通信的一些同步互斥手段慎用
最近对多线程调用时,动态链接库的全局变量有了以下几点认识。
1、动态链接库被同一进程的多个线程加载时,全局变量的值是进程有效。
例如:动态链接库C.dll有一个全局变量 int
g_iCount=0(初始值)。某一函数Method_D被调用一次,则g_iCount++。当某一进程加载C.dll后,线程A、B先后调用Method_D后,线程A获得的C.g_iCount=1,但线程B获得的C.g_iCount则是2。
这是因为线程B调用Method_D前,g_iCount已经被线程A更改。
2、虽然说动态链接库的全局变量是进程级的,但并不是说 同一进程里,不同模块定义同名全局变量就可以互相传递数值。
这跟我们的函数一样,即使在同一进程空间里,定义成相同名字跟结构,只要没有做导出,外部模块还是不能调用的。
所以,如果想在同一进程的不同线程间共享某一数据,也可以在全局变量定义时,使用_declspec(dllexport) 将其导出,
在调用处使用GetProcAddress()取得其在进程空间的地址来使用。
三、互斥锁共享
也就是说,想要在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;
}
若函数所在源文件被编译为共享库,那么该函数可以在共享库被其它进程显式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));
}
}