零基础Linux_21(多线程)页表详解+轻量级进程+pthread_create

news2024/11/23 19:17:53

目录

1. 页表详解

1.1 权限+条目+页框

1.2 页目录+页表项

2. 线程的概念

2.1 轻量级进程

2.2 Linux的线程

2.3 pthread_create

2.4 原生线程库+LWP和PID

3. 线程的公有资源和私有资源

3.1 线程的公有资源

3.2 线程的私有资源

4. 线程的优缺点

4.1 线程的优点

4.2 线程的缺点

本篇完。


1. 页表详解

我们在之前一直都提到页表,知道它的作用是将虚拟地址映射到物理地址,但是它具体怎么映射的,它的结构是什么样的,并没有提及过。

char* str = "hello world";
*str = 'H';

上诉代码,会在运行时报错,原因是str指向的地址在字符常量区,字符常量区的内容是不允许用户去修改的。

代码在运行起来以后,操作系统是怎么知道用户在修改字符常量区的呢?

1.1 权限+条目+页框

如上图所示的页表示意图,页表中不仅右虚拟地址和物理地址的映射关系,还有是否命中,RWX权限,U/K权限等等内容。

  • U/K权限:U表示用户(user),K表示内核(kernal)。
  • RWX权限:当前身份(用户或者内核)对当前地址的读,写执行权限。

上面代码在对srt指向的地址写内容时,先会经过页表映射到物理地址。但是在页表中发现这是一个写操作,并且该地址是不允许被写的,此时MMU就发送信号,导致程序报错。

虚拟地址,物理地址以及属性所在的一行,称为条目

依然是这张图,需要将这张图分解进行讲解。

物理内存空间划分:

以32位系统为例,它的物理内存理论上有4GB大小,但是这4GB又被分成了多块小空间。

被分割的小空间,每一块大小是4KB,被叫做页框

物理内存中会又很多个页框,并且这些页框也需要操作系统管理起来,采用的方式同样是先描述,再组织

通过一个结构体来描述页框

struct_Page
{
	//内存属性--4KB
}

代码形式如上所示,每一个页框都会有这样一个结构体对象,将多个结构体对象放在一个数组中:

struct_Page mem[];

此时就完成了先描述再组织的过程。操作系统中有专门的内存管理算法,叫做伙伴系统,有兴趣的可以自行查阅资料了解。

可执行文件:

写好的代码会经过编译器的处理形成二进制可执行文件放在磁盘中,在运行的时候加载到内存中。

编译器在处理源文件生成的二进制可执行文件,同样是以4KB为单位的,这4KB的数据块被叫做页桢

这一切都是设计好的,所以可执行程序在加载到内存中的时候是以4KB为单位的,正好一个页帧来填充一个页框。当页框被填充了以后,就会创建对应的struct_Page结构体对象,并且放在数组中,让操作系统来管理。

1.2 页目录+页表项

再回到页表,我们知道,每个进程对应的虚拟地址空间大小都是4GB的,也就是有2^32个地址,如果每个虚拟地址在页表中都对应着一个物理地址

那么页表就会有2^32行,每一行都是一个条目,每个条目中不仅有物理地址,虚拟地址,还有其他属性,假设一个条目的大小是10B,那么光页表就有10*2^32=40GB,已经超过了物理内存的大小,所以页表肯定不是这样的。

  • 实际上,页表是由页目录和页表项组成的。

在32位机器上,地址的大小是4G字节,也就是有32个比特位,

将32个比特位分为10个比特位,10个比特位,12个比特位,共3组:

 之前的的页表 = 页目录 + 页表项,如上图所示。

  • 32个比特位的高10位,作为页目录的下标,如上图所示的0000 0000,通过这个下标0可以访问到页目录中的第一个条目。
  • 页目录中存放的是页表项的地址,可以通过下标找到对应的页表项。

10个比特位,意味着页目录的下标范围是0~1023,最多是210也就是1KB个条目,大大减少了对内存的消耗。

  • 32个比特位的中间10位,作为页表项的下标,同样可以访问页表项中的条目。
  • 页表项中存放的是物理内存中页框的起始地址,可以通过下标找到物理内存中对应的页框。

同样,一个页表项最多有1KB个条目,指向1KB个页框。

  • 32个比特位中的低12位,作为偏移量,在物理内存中页框的起始地址基础上进行偏移,此时就可以得到具体数据在内存中的地址。
  • 这也是为什么页框和页帧的大小设置为4KB的原因,因为最低的12个比特位是2^12=4KB,偏移量最大就是4KB。

32位虚拟地址->物理地址的映射过程:

  1. 根据高10位下标找到页目录中对应页表项的地址
  2. 再根据中间10位下标找到页表相中对应页框的物理地址
  3. 再根据低12位偏移量进行偏移找到具体的物理地址。

页目录和页表项同样是采用先描述再组织的方式被操作系统管理起来的,每创建一个进程就会有一个页目录,只有在目录中存在的页表项才会被建立

采用这种方式,大大减少了对内存的消耗。

如何看到地址空间和页表:

  1. 地址空间是进程看到的资源窗口,每个进程都认为自己有4GB的资源。
  2. 页表决定进程真正拥有的资源。
  3. 合理对地址空间和页表进行资源划分,就可以对一个进程所有的资源进行分类。

2. 线程的概念

线程:是进程中的一个执行流。

Linux线程是CPU调度的基本单位。

回忆一下,之前我们对进程的定义是:内核数据结构 + 进程对应的代码和数据。

如上图所示,此时我们创建了多个“子进程”。

新创建的“子进程”中的mm_struct* mm都指向父进程的虚拟地址空间。也就是说,所有“子进程”和父进程共用一块虚拟地址空间。

父进程+和父进程共用一块虚拟地址空间的“子进程”,就叫做线程

此时开始,我们就将带引号的"子进程",叫做线程,因为以前的父子进程在这里都不这么叫了。

线程的作用:执行进程中的一部分代码。

线程在执行进程的部分代码时,有点像父子进程执行同一份代码中不同部分,区别在于线程和父进程使用的是同一份虚拟地址空间,而父子进程使用的是两个独立的虚拟地址空间。

2.1 轻量级进程

从图中可以看到,每个线程都也有一个task_struct结构体对象,用来描述线程的属性(id,状态,优先级,上下文,栈等等)。那么线程要不要被操作系系统管理起来呢?

答案是要的,而且采用的方式同样是先描述再组织,描述线程的task_struct结构体被叫做TCB–线程控制块,是英文Thread Contral Block的首字母。

描述好了以后同样像PCB一样,需要用链表组织起来进行管理,并也和PCB一样,有自己的管理算法。

但是,TCB中的属性和PCB几乎一样,管理TCB的数据结构和算法也和PCB的一样。
此时不仅会导致代码上的冗余,而且还会增加系统的开销,所以Linux并不是使用TCB管理线程的,因为这种方式比较复杂,维护起来不方便,而且运行也不是很稳定。

 Linux中,线程是直接复用PCB的数据结构和管理方法。
 所以在Liux操作系统中,进程和线程的描述结构体都是task_struct。

站在CPU的角度,它不关注被定义出来的进程还是线程这样的概念,它只关注task_struct。

CPU是一个被动的硬件,给它什么它就执行什么,所以它并不会区分当前执行的task_struct是一个进程还是一个线程,在它看来,都是进程

之前VS现在:

  • 之前:CPU执行的task_struct是一个进程。
  • 现在:CPU执行的task_struct是一个执行流。

所以说,今天CPU处理的task_struct 小于或等于 之前task_struct的含义。

站在内核的角度,称今天学习的task_struct为轻量级进程

我们可以通过虚拟地址空间 + 页表的方式对进程进行资源划分,让不同的“轻量级进程”同时执行不同部分的代码,所以单个“轻量级进程”的执行粒度,一定要比之前的进程细。

Linux内核中并没有线程的概念,线程是用进程PCB来模拟的。

由于Linux中,线程也是使用的PCB结构,是一种轻量化的进程,所以在Linux内核中并不存在线程的概念,也不存在线程的结构。

站在CPU的角度,每一个PCB都被称为轻量级进程。

CPU每次都是调度一个task_struct结构体,而这些PCB都是轻量级进程,有可能看作进程,也有可能看作线程,即使是看作进程,也可以看作是一个线程,因为无论是进程还是线程,都是一个个的执行流,CPU每次调度的都是一个执行流。

进程的重新定义:进程是承担分配系统资源的基本实体。

每创建一个进程,都会创建一个PCB,一个虚拟地址空间,一个页表,一块物理空间,而线程是属于这个进程中的执行流,它使用的是这个进程的资源。

所以此时的进程就包括因为创建它而产生的一系列开销(PCB,虚拟地址空间,页表,物理空间),这些都是属于这个进程的。

当这个进程中的某个线程申请新资源的时候,也是以该进程的名义去申请,而不是也这个线程的名义。

讲到这里,是不是觉得和之前学习的进程概念有冲突了?其实是自洽的。

之前我们学习的进程,每个进程只有一个执行流。

而现在每个进程中有多个执行流,每个执行流都是一个线程。

一个进程内可以有多个执行流,这些执行流都共用一个虚拟地址,一个页表。

最初的进程执行流被叫做主线程,之后创建的执行流被叫做新线程

主线程和新线程都属于一个进程,都是一体的,就像一个家庭中,有不同的成员,他们的工作是不同的,但是总目的都是一样的。

同样,多个线程同时工作的总目的也是相同的–为了完成这个进程的任务。

2.2 Linux的线程

通过上面介绍,我们知道,在Linux内核中是不存在线程这一个概念的,因为没有TCB数据结构以及管理算法,而我们所说的线程,都是在宏观层面,代指所有操作系统。

Linux操作系统中也没有提供创建线程的系统调用。

无论是宏观操作系统,还是用户(程序员)都只认线程的概念,但是Linux内核中并没有线程的概念。

我们(程序员)在编程的时候,仍然会使用线程的概念,那么我们在创建线程的时候,Linux内核中是怎么创建出轻量级进程的呢?

我们在创建进程的时候,会调用一个线程库,库中再通过一些系统调用创建出轻量级进程。

这样一来,程序员创建线程,Linux中创建轻量级进程,双方的要求就都满足了。

这个线程库是所有Linux操作系统必须自带的,所以也叫做原生线程库。

2.3 pthread_create

看看创建线程使用到的库函数接口,man pthread_create

  • pthread_t* thread:线程标识符tid,是一个输出型参数。
  • const pthread_attr_t* attr:线程属性,当前阶段一律设成nullptr。
  • void* (*start_routine)(void *):是一个函数指针,线程执行的就是该函数中的代码。
  • void* arg:传给线程启动函数的参数,是上面函数指针指向函数的形参。
  • 返回值:线程创建成功返回0,失败返回错误码。

先看一段pthread_create使用代码,Makefile:

mythread:mythread.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f mythread

mythread.cc:

#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

void *threadRun(void *args)
{
    const string name = (char*)args;
    while(true)
    {
        cout << name << ", pid: " << getpid() << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");

    while(true)
    {
        cout << "main pthread pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

将上诉代码进行编译的时候,我们发现报错了,编译器不认识pthread_create函数。

我们创建新线程只能通过原生线程库去创建,此时编译器找不到原生线程库。

我们要使用-l选项指定原生线程库pthread,之前在动态静态库的时候详细介绍过如果指定动态库。

Makefile:

mythread:mythread.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f mythread

再次编译运行上面代码:

此时就证明pid是一样的,再看下创建几个新线程的代码:

#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

void* threadRun(void *args)
{
    const string name = (char *)args;
    while (true)
    {
        cout << name << ", pid: " << getpid() << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid[5];
    char name[64];
    for (int i = 0; i < 5; i++)
    {
        snprintf(name, sizeof(name), "%s-%d", "thread", i); // 特定内容格式化到name中
        pthread_create(tid + i, nullptr, threadRun, (void*)name);
        sleep(1); // 缓解传参的bug
    }

    while (true)
    {
        cout << "main thread, pid: " << getpid() << endl;
        sleep(3);
    }
    return 0;
}

编译运行:

新线程和主线程都在同时运行,并没有陷入某一个死循环中。

2.4 原生线程库+LWP和PID

再前面基础上,ldd pthread(查看链接属性),再ll /lib64/libpthread.*(查看原生线程库)

查看可执行程序的链接属性。可以看到是动态链接,链接的库是原生线程库,如上图绿色框中所示。

根据线程库的路径去查看该路径下的所有文件,可以看到还有静态库,我们使用的线程库是一个软链接文件,它所链接的库才是真正的原生线程库。

在前面程序运行的时候打开一个窗口输入ps ajx | head -1 && ps ajx | grep mythread

主线程和新线程在同时运行,此时存在两个执行流。

但是在查看该进程的时候,发现mythread进程只有一个,pid,ppid等值也只有一个。

这也证明,线程是进程中的一个执行流,线程属于进程的一部分。

给线程发现kill -9 信号

给mythread进程发送9号信号,主线程和新线程都结束了。

  • 所有信号针对的都是进程,而线程属于进程。
  • 当一个进程结束以后,它的所有资源都会被回收,所以线程也就不存在了。

那我们想看到线程该怎么办呢?

再运行程序,使用指令ps -aL来查看线程。L必须大写,输入ps | aL

此时名字为mythread的线程有6个,它们的PID值相同,LWP不同。

  • PID:进程标识符
  • LWP:轻量级进程表示符,LWP是英文Light Weight Process的首字母。

可以看到,第一个线程的LWP和PID是一样的,这个线程就被叫做主线程

其它线程的LWP和PID不一样,这些线程就被叫做新线程

那么CPU在调度PCB的时候,根据的是LWP呢还是PID呢?

  • CPU在调度PCB的时候是根据LWP为标识符表示一个特点的执行流的。

因为CPU调度的都是轻量级进程,而每个轻量级进程也就线程的根本区别就在于LWP不同,但是不同线程的PID却有可能相同。

我们之前学习的进程,它只有一个执行流,也就是主线程,所以它的PID和LWP是相同的,即PID = LWP,我们使用哪个都无所谓。而现在我们学习了线程,就不能再只使用PID了,而是使用LWP。

3. 线程的公有资源和私有资源

3.1 线程的公有资源

所有线程都共享一个虚拟地址空间,一个页表,所以进程中的绝大部分资源都是所有线程共享的,先来看看共享的情况,写了一个公有函数,分别在主线程和新线程中调用这个函数:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

void show()
{
    cout << "show , pid: " << getpid() << " " <<  endl;
}

void* threadRun(void *args)
{
    const string name = (char *)args;
    while (true)
    {
        cout << name << ", pid: " << getpid() << endl;
        show();
        sleep(1);
    }
}

int main()
{
    // pthread_t tid[5];
    // char name[64];
    // for (int i = 0; i < 5; i++)
    // {
    //     snprintf(name, sizeof(name), "%s-%d", "thread", i); // 特定内容格式化到name中
    //     pthread_create(tid + i, nullptr, threadRun, (void*)name);
    //     sleep(1); // 缓解传参的bug
    // }
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");

    while (true)
    {
        cout << "main thread, pid: " << getpid() << endl;
        show();
        sleep(1);
    }
    return 0;
}

可以看到,主线程和新线程都可以调用这个函数。

  • 该进程中只有一份虚拟地址空间,该函数放在代码段中。
  • 所有线程共享一个代码段。

创建一个全局变量,在主线程和新线程中都打印,并且使用后置++。

将全局变量的地址在主线程和新线程中打印出来。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

int g_val = 100;

void show()
{
    cout << "show , pid: " << getpid() << " " << "g_val: " << g_val++ << " &g_val: " << &g_val <<  endl;
}

void* threadRun(void *args)
{
    const string name = (char *)args;
    while (true)
    {
        cout << name << ", pid: " << getpid() << "g_val: " << g_val++ << " &g_val: " << &g_val <<  endl;
        show();
        sleep(1);
    }
}

int main()
{
    // pthread_t tid[5];
    // char name[64];
    // for (int i = 0; i < 5; i++)
    // {
    //     snprintf(name, sizeof(name), "%s-%d", "thread", i); // 特定内容格式化到name中
    //     pthread_create(tid + i, nullptr, threadRun, (void*)name);
    //     sleep(1); // 缓解传参的bug
    // }
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");

    while (true)
    {
        cout << "main thread, pid: " << getpid() << "g_val: " << g_val++ << " &g_val: " << &g_val <<  endl;
        show();
        sleep(1);
    }
    return 0;
}

  • 新线程和主线程看到的全局变量是一个,当任意一个线程改变这个变量的值时,都会影响另一个线程使用这个值。
  • 主线程和新线程中,全局变量的地址是相同的,说明它们使用的是同一个全局变量。

根据上面现象以及分析,可以知道,数据段也是被所有线程共享的。

进程中的绝大部分资源都是和所有线程共享的。


3.2 线程的私有资源

因为所有线程都共享一个虚拟地址空间以及页表,线程之间有私有资源吗?答案肯定是有的。

  • PCB属性私有

所有线程都有各自的PCB,所以PCB中的属性肯定是私有的,属于各自线程。

  • 上下文数据私有

CPU在调度PCB的时候,采用轮转时间片的方式,当一个线程被换下时,该线程的上下文一定是私有的,防止被其他线程修改而导致恢复上下文的时候出现错误。

  • 栈结构私有

不同线程各自的临时变量一定是私有的,而临时变量存放在栈结构中,所有栈也是私有的。

都是同一块虚拟地址空间,怎么就让不同线程的栈结构私有了呢?这就涉及到了原生线程库的实现:man clone:

系统调用clone是用来创建子进程的,这里的子进程是轻量级进程,也就是没有独立的虚拟地址空间。

clone中有一个参数:void* child_stack,该参数就是用来自定这个子进程的栈空间的。

所以我们在使用pthread_create创建新线程的时候,底层会调用clone,并且会指定属于该线程的私有栈结构。

4. 线程的优缺点

4.1 线程的优点

与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

  • 进程切换:PCB切换 + 上下文切换 + 虚拟地址空间切换 + 页表切换
  • 线程切换:PCB切换 + 上下文切换

可以看到,线程切换比进程切换少了两项。 除此之外,线程切换时硬件缓冲区不用太多地更新。

cache:硬件缓冲区,其实就是我们所说的高速缓存。

它存在于CPU中,速度只是比CPU慢一点,但是比内存快很多。

cache会根据局部性原理从内存中拿数据,尤其是使用频率高的热点数据会一直放在cache中。CPU在使用数据时,不是直接去内存中拿,而是先去cache中拿,如果不命中,也就是不存在,cache就会将CPU所需要的数据从内存中缓存到cache中。

  •  进程间切换:不仅上面提到的四项内容需要切换,而且cache中的内容也需要重新缓存。
  •  线程间却换:切换PCB和上下文,但是cache中缓存的数据不需要切换。

所以线程都共用一个虚拟地址空间和一个页表,而cache中的内容也是根据虚拟地址和页表缓存进来的,所以不同进程之间是可以共用的。

这样一来,大大节省了cache从内存中缓存数据的时间,并且也节省了操作系统的大量工作。

当然还有很多其他的优点,比如:

  • 创建一个新线程的代价要比创建一个新进程小得多,因线程不创建新的虚拟地址空间和页表。
  • 线程占用的资源要比进程少很多。
  • 能充分利用多处理器的可并行数量。
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

计算密集型应用:主要体现在CPU的高频工作,如加密,解密,算法等。

I/O密集型应用:主要体现在和外设的交互上,如访问磁盘,显示器,网卡等。

上面很多线程的优点,进程也是拥有的。

4.2 线程的缺点

  • 健壮性或者鲁棒性较差

编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

还有比如一个线程里出现了异常退出,其它进程也会退出:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

int g_val = 100;

void show()
{
    cout << "show , pid: " << getpid() << " "
         << "g_val: " << g_val++ << " &g_val: " << &g_val << endl;
}

void *threadRun(void *args)
{
    const string name = (char *)args;
    while (true)
    {
        cout << name << ", pid: " << getpid() << "g_val: " << g_val++ << " &g_val: " << &g_val << endl;
        show();
        sleep(1);
        static int cnt = 0;
        if (cnt == 7)
        {
            int *p = nullptr;
            *p = 777;
        }
        cnt++;
    }
}

int main()
{
    pthread_t tid[5];
    char name[64];
    for (int i = 0; i < 5; i++)
    {
        snprintf(name, sizeof(name), "%s-%d", "thread", i); // 特定内容格式化到name中
        pthread_create(tid + i, nullptr, threadRun, (void *)name);
        sleep(1); // 缓解传参的bug
    }
    // pthread_t tid;
    // pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");

    while (true)
    {
        cout << "main thread, pid: " << getpid() << "g_val: " << g_val++ << " &g_val: " << &g_val << endl;
        show();
        sleep(1);
    }
    return 0;
}

​​​​​​​

新线程中发送端错误异常,收到了11号信号SIGSEGV。

但是不仅这个新线程结束了,主线程和其它线程也结束了。

线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。而多进程就不存在,一个进程的退出并不会影响另一个进程。

  • 性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销(线程切换),而可用的资源不变。

  • 缺乏访问控制

进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

  • 编程难度提高

编写与调试一个多线程程序比单线程程序困难得多、

一般情况下,CPU有几个核就创建几个线程。

核:只的是一个CPU中的运算器个数,即使是多核,而控制器也是一个CPU只有一个。

本篇完。

下一篇:零基础Linux_22(多线程)线程控制和和C++的多线程和笔试选择题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1123736.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LLM 新缺陷曝光,自我纠正成功率仅 1%;苹果超 95% 产品仍在中国制造丨 RTE 开发者日报 Vol.72

开发者朋友们大家好&#xff1a; 这里是 「RTE 开发者日报」 &#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE &#xff08;Real Time Engagement&#xff09; 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文…

JSX 条件渲染

学习目标&#xff1a; 能够在 JSX 中实现条件渲染 作用&#xff1a; 根据是否满足条件生成 HTML 结构&#xff0c;比如 Loading 效果 实现&#xff1a; 可以使用三元运算符或者逻辑&&&#xff08;与&#xff09;运算符 实例&#xff1a; function App() {// 条件渲染/…

基于模型的电机brushless DC motor (BLDCM)控制方法

(无框)力矩电机&#xff0c;全称应该为永磁无刷直流(无框)力矩电机&#xff0c;其显著的特征是&#xff1a; 较大的径长比&#xff08;diameter-to-length ratio&#xff09;较多数目的磁极&#xff08;magnet poles&#xff09; 无框架电机是传统电机中用于产生扭矩和速度的…

朋友圈可分享的产品画册是怎么做的?

经常刷朋友圈的时候有看到别人分享的各式各样的产品画册&#xff0c;带动画、带音乐、还有真书般的翻页效果&#xff0c;看起来十分炫酷且好看呢&#xff01;那么&#xff0c;这种可以分享到朋友圈的产品画册是怎么制作的呢&#xff1f; 推荐使用FLBOOK即可在线制作并分享到朋友…

电商API接口:如何让数据成为生产力!

随着数字化商业时代的到来&#xff0c;API接口已成为电商资源连接利器&#xff0c;也是全球传统互联网企业转型的基础。 2021年 Google Cloud 研究显示&#xff0c;全球互联网企业近3/4的企业持续投入数字化转型&#xff0c;2/3的企业在持续增加投入&#xff0c;从这组数据可以…

从制造到“智造”,看科聪控制系统如何赋能汽车行业智能生产

汽车行业成为推动国家经济发展的支柱性行业之中&#xff0c;在国家经济社会发展有着重要的地位。如今&#xff0c;汽车制造业面临着制造升级和技术更新的问题&#xff0c;需要高效、合理和现代化的物流系统。 某汽车厂通过投运以科聪控制系统为核心的叉式移动机器人&#xff0c…

山西电力市场日前价格预测【2023-10-24】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2023-10-24&#xff09;山西电力市场全天平均日前电价为246.84元/MWh。其中&#xff0c;最高日前电价为419.16元/MWh&#xff0c;预计出现在18:30。最低日前电价为0.00元/MWh&#xff0c;预计出…

【wxWidgets实现透明wxPanel_核心实现_原创思想】

描述 wxWidgets 根本就没有实现过透明wxPanel容器,你设置wxTRANSPARENT_WINDOW,结果sorry 黑色,哈哈哈哈, 就是和你作对.想想当下那么漂亮的桌面, 背景, 透明, 特效.哎 悲哀啊,实现不了,就那死板的界面特性. 网上找了好久,也是乱七八糟,改底层代码还是算了吧,升级特要命.都是只…

Ultralytics YOLOv8的关键特点

计算机视觉领域正在迅速增长&#xff0c;其中最重要的技术之一是目标检测。每六个月都会出现新的目标检测算法&#xff0c;不断提高准确性&#xff0c;与之前的算法相比。Ultralytics YOLOv8是最先进的目标检测算法&#xff0c;不仅提供卓越的准确性&#xff0c;还支持CPU和GPU…

一次单据图片处理的优化实践 | 京东物流技术团队

1 引言 日常开发中接到这样的需求&#xff0c;上游系统请求获取一张A4单据用于仓库打印及展示&#xff0c;要求PNG图片格式&#xff0c;但是我们内部得到的单据格式为PDF&#xff0c;需要提取PDF文档的元素并生成一张PNG图片。目前已经有不少开源工具实现了这一功能&#xff0…

【Mybatis】不启动项目直接测试Mapper的方法

【Mybatis】不启动项目直接测试Mapper的方法 0、前言1. 依赖2. 数据库3. 实体类4. Mapper文件5. 配置类6. Main方法 0、前言 在项目开发过程中&#xff0c;有时候一个庞大的SpringBoot 项目的启动时间可能要几分钟的时间&#xff0c;这时候我们如果想测试自己写的某个mybatis的…

pytorch实战---IMDB情感分析

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

【基于形态学的权重自适应去噪】

【基于形态学的权重自适应去噪】 1 引言2 数学形态学原理3 权重自适应的多结构形态学去噪4 实现代码4.1 主函数代码4.2 串、并联去噪4.3 图像权值计算4.4 计算 PSNR 值 5 实验结果 参考书籍&#xff1a;计算机视觉与深度学习实战:以MATLAB、Python为工具&#xff0c; 主编&…

【Java】多态中调用成员的特点

示例代码 public class Test {public static void main(String[] args) {//创建对象&#xff08;多态方式&#xff09;//父类 f new 子类();Animal a new Dog();//调用成员变量&#xff1a;编译看左边&#xff0c;运行也看左边//编译看左边&#xff1a;javac编译代码的时候&…

Python深度学习进阶与应用丨注意力(Attention)机制、Transformer模型、生成式模型、目标检测算法、图神经网络、强化学习详解等

目录 第一章 注意力&#xff08;Attention&#xff09;机制详解 第二章 Transformer模型详解 第三章 生成式模型详解 第四章 目标检测算法详解 第五章 图神经网络详解 第六章 强化学习详解 第七章 深度学习模型可解释性与可视化方法详解 更多应用 近年来&#xff0c;伴…

【Java】JDK 21中的虚拟线程以及其他新特性

目录 一、字符串模板&#xff08;String Templates&#xff09; 二、序列化集合&#xff08;Sequenced Collections&#xff09; 三、分代ZGC&#xff08;Generational ZGC&#xff09; 四、记录模式&#xff08;Record Patterns&#xff09; 五、Fibers&#xff08;纤程&…

实战SRC

附言&#xff1a;从补天的公益src公司中选中了幸运儿。 1. 通过hunter鹰图平台搜索公司的相关资产&#xff0c;发现其采用了华途应用安全网关。 2.访问相关地址&#xff0c;尝试使用弱口令登录&#xff0c;发现直接利用admin/admin就登录了&#xff0c;可以看到后台的相关日志…

汉语言语的声学特点是什么

汉语言语的声学特点是什兰明 医学硕士&#xff0c;听力学博士&#xff0c;听觉健康门诊主任 虽然互联网已经将英语作为最常用的&#xff08;第二&#xff09;语言的地位&#xff0c;但中文&#xff08;普通话&#xff09;仍然是最常用的母语。2010年&#xff0c;以中文为…

新成果展示:AlGaN/GaN基紫外光电晶体管的设计与制备

紫外光电探测器被广泛应用于导弹预警、火灾探测、非可见光通信、环境监测等民事和军事领域&#xff0c;这些应用场景的实现需要器件具有高信噪比和高灵敏度。因此&#xff0c;光电探测器需要具备响应度高、响应速度快和暗电流低的特性。近期&#xff0c;天津赛米卡尔科技有限公…

C++ 读取数量不定的输入数据

在C中&#xff0c;有时我们会遇到&#xff0c;在事先没有知道&#xff0c;要对多少个数进行求和的情况下&#xff0c;这就需要不断的读取数据直至没有新的输入为止&#xff1a; demo&#xff1a; #include <iostream> using namespace std;int main() {int sum 0;in…