线程
这个小节的目标是稍微熟悉一下c语言体系下的多线程操作,避免看到类似代码一脸懵逼 不知道为啥
使用pthread 创建一个线程
注意这里要改一下 cmake文件 否则,pthread 相关依赖是找不到的
add_executable(cstudy12 pthread_test.c)
target_link_libraries(cstudy12 pthread)
//
#include <pthread.h>
#include <stdio.h>
void *SayHello(void *name) {
printf("hello from thread %s", name);
pthread_exit(NULL);
}
int main() {
//声明了一个pthread_t类型的变量tid来存储线程ID
pthread_t tid;
// ret!=0 代表创建线程失败了
int ret;
//第一个参数是指向pthread_t变量的指针,用于存储新线程的ID。第二个参数是线程的属性,可以使用NULL表示默认属性。
// 第三个参数是指向线程入口点函数的指针。最后一个参数是传递给线程入口点函数的参数,可以使用NULL表示没有参数。
ret = pthread_create(&tid, NULL, SayHello, "wuyue");
if (ret) {
printf("error create pthraed");
return 1;
}
//我们使用pthread_join函数等待新线程完成执行。pthread_join函数的第一个参数是要等待的线程的ID,
// 第二个参数是指向线程返回值的指针,如果不需要获取线程返回值,可以使用NULL表示不关心返回值。
pthread_join(tid, NULL);
return 0;
}
锁操作
#include <pthread.h>
#include <stdio.h>
int global_value=0;
pthread_mutex_t lock;
void *thread_func(void* arg){
int i;
for (i = 0; i < 100; ++i) {
//locl和unlock要成对出现
pthread_mutex_lock(&lock);
global_value++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main() {
// 注意init和destroy 要成对出现
pthread_mutex_init(&lock,NULL);
pthread_t t1,t2;
pthread_create(&t1,NULL, thread_func,NULL);
pthread_create(&t2,NULL, thread_func,NULL);
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_mutex_destroy(&lock);
printf("value:%d",global_value);
return 0;
}
threadlocal
#include <pthread.h>
#include <stdio.h>
//__thread关键字告诉编译器为thread_local_var变量分配线程本地存储空间,这样每个线程都可以拥有自己的thread_local_var变量实例。
// 这意味着在不同的线程中访问thread_local_var变量时,每个线程都会访问自己的thread_local_var变量实例,从而避免了线程间的竞争和同步开销。
__thread int thread_local_var;
//在线程函数中,我们可以像使用普通变量一样使用thread_local_var变量,并且每个线程都可以拥有自己的变量实例
void *thread_func(void *arg){
thread_local_var = (int) arg;
printf("Thread %d: thread_local_var=%d\n", (int) pthread_self(), thread_local_var);
pthread_exit(NULL);
}
int main() {
pthread_t tid[2];
int i;
//创建两个线程
for (i = 0; i < 2; i++) {
if (pthread_create(&tid[i], NULL, thread_func, (void*)i) != 0) {
perror("pthread_create");
}
}
//等待线程结束
for (i = 0; i < 2; i++) {
pthread_join(tid[i], NULL);
}
return 0;
}
C语言的编译过程
这一小节 主要是为了后面静态库和动态库理解用的
预处理器
这一步执行完以后就是宏 替换后的源代码
gcc -E helloworld.c -o helloworld.i
可以看下这个helloworld.i 是啥
内容挺长的,大家可以自行看看,其实也是源代码 只不过比你写的源代码要复杂多了,宏被统一替换成源代码了
编译器
这一步执行完以后就是中间文件 也就是汇编指令
gcc -S helloworld.i -o helloworld.s
也可以直接编译成目标文件
目标文件是计算机可执行的二进制文件,它是编译器生成的中间文件,包含编译后的机器代码和未解析的符号引用。 目标文件可以用于生成最终的可执行文件或者库文件。
gcc -C helloworld.s -o helloworld.o
我们可以看一下这个 目标文件的类型
未解析的符号引用 这概念要好好掌握:
未解析的符号引用(Unresolved symbol reference)指的是在目标文件中引用的一个符号(通常是函数或变量)没有在该文件中定义。这个符号被标记为“未解析”,因为编译器无法确定该符号所代表的实际地址,因为该符号的定义在其他文件中。在链接器(Linker)将多个目标文件组合成可执行文件或者库文件时,需要解析这些未解析的符号引用,将其与正确的定义进行匹配,以确保程序能够正常运行。
未解析的符号引用通常发生在使用库文件时,因为库文件是预编译好的二进制代码,编译器无法在编译时确定库中函数或变量的定义。当链接器链接目标文件时,它会搜索库文件,以找到符号的定义,并将其与未解析的引用进行匹配。
如果链接器无法找到符号的定义,它将生成一个“未定义符号”错误并停止链接过程。因此,解决未解析的符号引用问题非常重要,通常需要使用正确的编译器选项和库文件来确保正确的链接。
我们可以使用命令来查看 有哪些未解析的符号引用
nm -u helloworld.o
这里有的人奇怪 怎么会有个puts函数,我们写的是printf啊, 这是因为我们printf里面 是个纯字符串比较简单,所以gcc的编译器自动给我们优化了
查看下汇编代码就真相大白了
链接器
这一步执行完以后就是可执行文件了
我们可以把这一步打出来看一下
gcc -v helloworld.o -o helloworld
代码我就不贴了,太长了,基本上都跟一个叫collect的程序有关。
可以看下生成的可执行文件类型是什么
静态链接库与动态链接库
静态库被使用目标代码最终和可执行文件在一起(它只会有自己用到的),而动态库与它相反,它的目标代码在运行时或者加载时链接。
静态链接的可执行文件要比动态链接的可执行文件要大得多,因为它将需要用到的代码从二进制文件中“拷贝”了一份,而动态库仅仅是复制了一些重定位和符号表信息。
静态链接的可执行文件不需要依赖其他的内容即可运行,而动态链接的可执行文件必须依赖动态库的存在。所以如果你在安装一些软件的时候,提示某个动态库不存在的时候也就不奇怪了。
即便如此,系统中一般存在一些大量公用的库,所以使用动态库并不会有什么问题。
静态链接库
已知 我们有如下 一段c 代码
add.c 和 subtract.c 代码就不放了 很简单,大家猜也能猜到 啥意思
现在我们想打出一个可执行程序
我们首先打出2个目标文件
gcc -c add.c subtract.c
然后我们使用ar命令将这两个目标文件打包成静态链接库 libmath.a
ar rcs libmath.a add.o subtract.o
然后
gcc -o main main.c -L. -lmath
即可编译出我们的可执行文件了
动态链接库
注意动态库的命名必须是lib开头
其实你看 就算刚才我们编译出来的 main 可执行文件 里面也是需要有动态链接库信息的
这个libc 应该很熟悉吧。。
还是上面那个例子
gcc -v -fPIC -shared -o libmath.so add.c subtract.c
-v 可以详细打出编译的过程日志信息,有兴趣的可以自己看下
可以看出来 这个so 文件已经出来了
继续编我们的可执行程序:
gcc -o main main.c -L. -lmath
但是当你执行的时候 你会发现报错了
他说这个 找不到这个so文件在哪里
此时我们再查看一下
要解决这个方法也很简单
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/library
再看 即可恢复正常
当然你也可以把这个so 文件拷贝到 /usr/lib 这个文件夹下, 其实很多程序的安装 就是把动态库 放到/usr/lib下
mmap 技术
mmkv 用的就是这个作为基础, 这里也用c语言来做一些演示,加深一下理解。讲白了对于andorid程序员来说,其实就是 用c语言和linux的接口 以及 android 提供的一些c语言的库,来做一些编程
优点:
在mmap技术中,操作系统将文件映射到进程的虚拟地址空间中,然后进程可以像访问内存一样访问文件的内容。这使得文件的访问变得更加高效,因为操作系统可以通过页面映射技术将文件内容读入物理内存中,并且在需要时将其刷回磁盘,而不需要频繁地进行文件I/O操作
缺点:
- 映射大文件时可能会消耗大量的虚拟内存。因为mmap技术将文件内容映射到进程的地址空间中,所以在映射大文件时会消耗大量的虚拟内存,可能会导致进程内存耗尽的问题。
- 映射文件时可能会导致文件锁定。当使用mmap技术将文件映射到进程的地址空间中时,文件可能会被锁定,导致其他进程无法对其进行访问。
- 不能直接访问文件系统。mmap技术只能访问已经打开的文件,不能直接访问文件系统,这可能会导致某些应用程序无法使用该技术来访问文件。
- 不支持对文件末尾进行动态扩展 使用mmap技术将文件映射到进程的地址空间中时,如果需要对文件末尾进行动态扩展,则需要重新映射文件,这可能会导致额外的开销和复杂性。
在使用mmap技术时,通常需要满足一些要求,其中一个要求是要按照4K的倍数进行映射。这是因为操作系统将进程的地址空间分为多个页面,每个页面通常是4K字节大小,因此如果要将文件映射到进程的地址空间中,就需要按照页面大小的倍数进行映射。
如果按照不是4K的倍数进行映射,操作系统可能会自动进行调整,但这可能会导致额外的开销和性能问题。因此,在使用mmap技术时,最好按照4K的倍数进行映射,以获得最佳的性能和效率。
通过上述的描述,我们要遵循一个使用方法, 当你使用mmap技术时,一定要适当的调整好自己的文件大小,这个文件大小一旦确定下来,后续就不要去改他了。
下面看一个例子
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#define FILE_SIZE 1024
int main() {
// 此处一定要注意 有读写文件的权限才可以
int fd = open("file.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
if (ftruncate(fd, FILE_SIZE) == -1) {
perror("ftruncate");
exit(EXIT_FAILURE);
}
// 一般第一个参数都是为null 这里也是要有读写文件
char *file_data = mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (file_data == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
file_data[0] = 'H';
file_data[1] = 'e';
file_data[2] = 'l';
file_data[3] = 'l';
file_data[4] = 'o';
// 将修改 同步回文件
if (msync(file_data, FILE_SIZE, MS_SYNC) == -1) {
perror("msync");
exit(EXIT_FAILURE);
}
// 取消映射
if(munmap(file_data,FILE_SIZE)==-1){
perror("munmap");
exit(EXIT_FAILURE);
}
// 关闭文件
if(close(fd)==-1){
perror("close");
exit(EXIT_FAILURE);
}
return 0;
}
如果是打开一个已存在的文件怎么做? 其实和上面的代码差不多,主要就是自己获取文件的实际大小就可以了
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
int main() {
// 此处一定要注意 有读写文件的权限才可以
int fd = open("file.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 获取文件的大小
struct stat st;
if (fstat(fd, &st) == -1) {
perror("fstat");
exit(EXIT_FAILURE);
}
size_t file_size = st.st_size;
// 一般第一个参数都是为null 这里也是要有读写文件
char *file_data = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (file_data == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
file_data[0] = 'w';
file_data[1] = 'u';
file_data[2] = 'y';
file_data[3] = 'u';
file_data[4] = 'e';
// 将修改 同步回文件
if (msync(file_data, file_size, MS_SYNC) == -1) {
perror("msync");
exit(EXIT_FAILURE);
}
// 取消映射
if (munmap(file_data, file_size) == -1) {
perror("munmap");
exit(EXIT_FAILURE);
}
// 关闭文件
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE);
}
return 0;
}
有兴趣的可以自己试着 用mmap 封装一层 简单的接口 给java 端调用,实现一个简单版本的mmkv。 简单版本的mmkv 我们就用 mmap+文本的方式就可以了。 实际的mmkv 不过是把文本存储 替换成了 pb 协议二进制存储,效率更高,仅此而已
总结
jni 的第一部分 知识到这里就结束了,主要就是简单介绍了下c语言以及 linux下的文件操作, 有了这些基础知识 已经可以让我们看一下简单的 jni库了, 要真正的上手去写jni程序,最终还是得学习一下c++,后续的基础知识就围绕c++展开, 同时穿插一些 重要的linux基础,例如 进程,权限,共享内存,信号 等等,这些知识对后续实际的android端 jni编程非常有帮主