线程
这个末节的方针是略微熟悉一下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编程十分有帮主