[Linux][多线程][一][线程基础概念][进程VS线程][线程控制]详细讲解

news2024/12/23 15:45:20

目录

  • 0.预备知识
    • 1.页表的映射
    • 2.二级页表
  • 1.线程基础概念
    • 1.什么是线程?
    • 2.理解流程梳理 -- 如何理解线程?
    • 3.线程优点
    • 4.线程缺点
    • 5.线程异常
    • 6.线程用途
  • 2.进程VS线程
    • 1.进程和线程
    • 2.进程和线程的资源共享
    • 3.进程和线程的关系
    • 4.关于进程线程的问题
  • 3.线程控制
    • 1.POSIX线程库
    • 2.线程创建
    • 3.线程终止
      • pthread_exit()
      • pthread_cancel()
    • 4.线程等待
      • pthread_join()
    • 5.线程分离
      • pthread_detach()
    • 6.线程ID深入理解
      • pthread_self()
    • 7.进程地址空间布局


0.预备知识

1.页表的映射

  • 在32位平台下一共有232个地址,也就意味着有232个地址需要被映射

请添加图片描述

  • 每一个表项中除了要有虚拟地址和与其映射的物理地址以外,实际还需要有一些权限相关的信息,如用户级页表和内核级页表,实际就是通过权限进行区分的
    请添加图片描述

  • 每个应表项中存储一个物理地址和一个虚拟地址就需要8个字节,考虑到还需要包含权限相关的各种信息,这里每一个表项就按10个字节计算

    • 这里一共有232个表项,也就意味着存储这张页表需要用232 * 10个字节,也就是40GB
    • 而在32位平台下我们的内存可能一共就只有4GB,也就是说我们根本无法存储这样的一张页表

2.二级页表

  • 虚拟地址在被转化的过程中,不是直接转化的!而是拆分成了10 + 10 + 12
  • 以32位平台为例,其页表的映射过程如下:
    • 选择虚拟地址的前10个比特位在页目录当中进行查找,找到对应的页表
    • 再选择虚拟地址的10个比特位在对应的页表当中进行查找,找到物理内存中对应页框的起始地址。
    • 最后将虚拟地址中剩下的12个比特位作为偏移量从对应页框的起始地址处向后进行偏移,找到物理内存中某一个对应的字节数据
  • 相关说明:
    • 物理内存实际是被划分成一个个4KB大小的页框的,而磁盘上的程序也是被划分成一个个4KB大小的页帧的,当内存和磁盘进行数据交换时也是以4KB大小为单位进行加载和保存的
    • 4KB = 212个字节,一个页框中有212个字节,而访问内存的基本大小是1字节,因此一个页框中就有2^12个地址,于是就可以将剩下的12个比特位作为偏移量,从页框的起始地址处开始向后进行偏移,从而找到物理内存中某一个对应字节数据
  • 这实际上就是所谓的二级页表,其中页目录项是一级页表页表项是二级页表
    • 每一个表项还是按10字节计算,页目录和页表的表项都是210个,因此一个表的大小就是210 * 10个字节,也就是10KB
    • 页目录有210个表项也就意味着页表有210个,也就是说一级页表有1张,二级页表有2^10张,总共算下来大概就是10MB,内存消耗并不高,因此Linux中实际就是这样映射的
  • **注意:**Linux中,32位平台用的是二级页表,64位平台用的是多级页表
    请添加图片描述

1.线程基础概念

1.什么是线程?

  • 个程序里的一个执行流就叫做线程(thread)
    • 即:线程是"一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
    • 线程在进程内部执行,是OS调度的基本单位
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
    请添加图片描述

2.理解流程梳理 – 如何理解线程?

  • 每个进程都有自己独立的进程地址空间和独立的页表,也就意味着所有进程在运行时本身就具有独立性

    • 在创建进程时,它要创建PCB,页表,建立代码和数据的映射关系…
    • 所以创建一个进程的成本非常高
  • 如果创建"进程"时,只创建task_struct,并要求创建出来的task_struct和父task_struct共享进程地址空间和页表

    • 现在创建的进程不再给你独立分配地址空间和页表,而是都指向同一块地址空间,共享同一块页表
    • 所以这四个task_struct看到的资源都是一样的,后续可以通过某种方式把代码区拆分成4块,让这四个task_struct执行不同的代码区域
    • 上述的区域(数据区,堆区,栈区)也是类似处理方式
    • 换言之,后续创建的3个task_struct都各自有自己的一小份代码和数据,把这样的一份task_struct称之为线程
    • 其中每一个线程都是当前进程里面的一个执行流,也就是常说的"线程是进程内部的一个执行分支"
      请添加图片描述
  • 线程在进程内部运行,本质就是线程在进程地址空间内运行,也就是说曾经这个进程申请的所有资源,几乎都是被所有线程共享的

    • 线程比进程更细,是因为其执行的代码和数据更小了
    • 线程的调度成本更低了,是因为它将来在调度的时候,核心数据结构(地址空间和页表)均不用切换了
  • 上述线程仅仅是在Linux下的实现,不同平台对线程管理可能不一样

    • 如Windows有真正的有关多线程的数据结构
    • 而Linux并没有真正的对线程创建对应的数据结构
    • Linux的线程是用进程PCB模拟的
    • 所以Linux并不能直接提供线程相关的接口,只能提供轻量级进程的接口
      • 在用户层实现了一套用户层多线程方案,以库的方式提供给用户进行使用
      • pthread线程库
  • CPU视角下,Linux下,PCB <= 其他OS内的PCB

    • Linux下的进程,统一称之为:轻量级进程

3.线程优点

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

4.线程缺点

  • 性能损失
    • 一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器
    • 如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失
      • 这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变
  • 健壮性降低
    • 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的
  • 缺乏访问控制
    • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响
  • 编程难度提高
    • 编写与调试一个多线程程序比单线程程序困难得多

5.线程异常

  • 线程一旦异常,会导致整个进程整体退出
    • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
    • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

6.线程用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验
    • 如:一边写代码一边下载开发工具,就是多线程运行的一种表现

2.进程VS线程

1.进程和线程

  • 进程是资源分配的基本单位

  • 线程是调度的基本单位

    • 线程ID
    • 一组寄存器
    • errno
    • 信号屏蔽字
    • 调度优先级
  • 为什么线程切换的成本更低?

    • 地址空间和页表不需要切换
    • CPU内部是有L1~L3 cache,如果进程切换,cache就立即失效,新进程过来,只能重新缓存

2.进程和线程的资源共享

  • 进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的
    • 如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到
  • 除此之外,各线程还共享以下进程资源和环境:
    • 文件描述符表
    • 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
    • 当前工作目录
    • 用户id和组id
  • 补充说明:
__thread int g_val = 100; // 修饰全局变量,让每一个线程各自拥有一个全局的变量  --  线程的局部存储

3.进程和线程的关系

请添加图片描述

4.关于进程线程的问题

  • 如何看待之前学习的单进程?

    • 具有一个线程执行流的进程
  • 引入线程后,如何重新理解之前的进程?

    • 红色方框框起来的内容,将这个整体称作进程
    • 曾经理解的进程 = 内核数据结构 + 进程对应的代码和数据
    • 现在的进程,从内核角度看:承担分配系统资源的基本实体
      • 所有进程最大的意义是向系统申请资源的基本单位
        请添加图片描述
  • 一个进程内部一定存在多个执行流,那么这些执行流在CPU角度有区别吗?

    • 没有任何区别,CPU不关心当前是进程还是线程这样的概念,只关心PCB,CPU调度的时候照样以task_struct为单位来进行调度
      • 只是这里task_struct背后的代码和页表只是曾经的代码和页表的一小部分而已
      • 所以CPU执行的只是一小块代码和数据,但并不妨碍CPU执行其它执行流
      • 所以就可以把原本串行的所有代码转变成并发或并行的,让这些代码在同一时间点得以推进
    • 总结如下:
      • 以前CPU看到的所有的task_struct都是一个进程,现在CPU看到的所有的task_struct都是一个执行流(线程)

3.线程控制

1.POSIX线程库

  • pthread****线程库是应用层的原生线程库:
    • 应用层指的是这个线程库并不是系统接口直接提供的,而是由第三方提供的
    • 原生指的是大部分Linux系统都会默认带上该线程库。
    • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的。
  • 错误检查:
    • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误
    • C语言中errno是个全局变量,线程很可能有多个,大家共用一个errno,这个errno就成了临界资源,在实际使用中就不安全了
      • pthreads函数出错时不会设置全局变量errno(而大部分POSIX函数会这样做),而是将错误代码通过返回值返回
      • 对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小
    • pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码

2.线程创建

  • **功能:**创建一个新的线程
  • 函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);
  • 参数:
    • **thread:**输出型参数,获取创建成功的线程ID
    • **attr:**设置线程的属性,attr为nullptr表示使用默认属性
    • **start_routine:**是个函数地址,线程启动后要执行的函数
    • **arg:**传给线程启动函数的参数
  • **返回值:**成功返回0,失败返回错误码

3.线程终止

  • 如果需要只终止某个线程而不终止整个进程,可以有三种方法:
    • 从线程函数return
      • 这种方法对主线程不适用,从main函数return相当于调用exit
    • 线程可以调用pthread_ exit终止自己
    • 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程
  • 注意:
    • 不能使用exit()终止线程!
    • **exit()**的作用是终止进程,任何一个线程调用exit()都会导致整个进程终止

pthread_exit()

  • **功能:**进程终止
  • 原型:void pthread_exit(void *retval);
  • **参数:retval:**线程退出时的退出码信息
  • **返回值:**无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
  • 注意:
    • pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配
      • 因为当其它线程得到这个返回指针时线程函数已经退出了
    • 若主线程调用pthread_exit,则只退出主线程,并不会导致进程的退出

pthread_cancel()

  • **功能:**取消一个执行中的线程
  • 原型:int pthread_cancel(pthread_t thread);
  • 参数:thread:线程ID
  • **返回值:**成功返回0,失败返回错误码

4.线程等待

  • 一个线程被创建出来,这个线程就如同进程一般,也是需要被等待的
  • 为什么需要线程等待?
    • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内
    • 创建新的线程不会复用刚才退出线程的地址空间
    • 如果主线程不对新线程进行等待,那么这个新线程的资源也是不会被回收的
    • 所以线程需要被等待,如果不等待会产生类似于"僵尸进程"的问题,也就是内存泄漏

pthread_join()

  • **功能:**等待线程结束,默认阻塞等待
  • 原型:int pthread_join(pthread_t thread, void **retval);
  • 参数:
    • thread**:**线程ID
    • retval**:**利用其带回线程返回值,需深刻理解
  • 返回值:
    • 线程等待成功返回0,失败返回错误码
  • 说明:
    • 调用该函数的线程将阻塞式挂起等待,直到ID为thread的线程终止
    • thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的
      • 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值
      • 如果thread线程被别的线程调用pthread_ cancel异常终止,retval所指向的单元里存放的是常数PTHREAD_ CANCELED
      • 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数
      • 如果对thread线程的终止状态不感兴趣,可以传nullptr给retval参数
  • 带回返回值例子
void *threadRoutine(void* args)
{
    return (void *)233; // 返回给主线程
}

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

    void *ret = nullptr;
    pthread_join(tid, &ret); // 默认会阻塞等待新线程退出

    cout << "new thread retval:" << (long long)(ret) << endl;
    return 0;
}

5.线程分离

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统资源泄漏
  • 如果不关心线程的返回值,join是一种负担
    • 这个时候,可以告诉系统,当线程退出时,自动释放线程资源
    • 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
  • 一个线程如果被分离了,这个线程依旧要使用该进程的资源,依旧在该进程内运行,甚至这个线程崩溃了一定会影响其他线程
    • 只不过这个线程退出时不再需要主线程去join了,当这个线程退出时系统会自动回收该线程所对应的资源
  • joinable和分离是冲突的,一个线程不能既是joinable又是分离的

pthread_detach()

  • **功能:**分离线程
  • 原型:int pthread_detach(pthread_t thread);
  • 参数:thread:线程ID
  • **返回值:**成功返回0,失败返回错误码

6.线程ID深入理解

  • pthread_ create()会产生一个线程ID,存放在第一个参数指向的地址中
    • 该线程ID和内核中的LWP不是一回事
      • 内核中的LWP属于进程调度的范畴
      • 因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程
  • pthread_ create()第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID, 属于NPTL线程库的范畴
    • 线程库的后续操作,就是根据该线程ID来操作线程的
  • 如何获取线程ID? – 通常有两种
    • 创建线程时通过输出型参数获得
    • 通过调用pthread_self()取得

pthread_self()

  • **功能:**获得线程自身ID
  • 函数原型:pthread_t pthread_self(void);
  • **说明:**用pthread_self()获得的线程ID与内核的LWP的值是不相等的
    • pthread_self()获得的是用户级原生线程库的线程ID
    • LWP是内核的轻量级进程ID

7.进程地址空间布局

  • pthread_t****到底是什么?
    • 本质就是一个进程地址空间上的一个地址
    • 同一个进程中所有的虚拟地址都是不同的,因此可以用它来唯一区分每一个线程
    • 所谓线程ID可以理解为每个新线程在库当中的内存位置的起始地址,线程控制块的起始地址
  • 每个线程都有自己私有的栈,其中主线程采用的栈是进程地址空间中原生的栈,而其余线程采用的栈就是在共享区中开辟的
    • 除此之外,每个线程都有自己的struct pthread,当中包含了对应线程的各种属性
    • 每个线程还有自己的线程局部存储,当中包含了对应线程被切换时的上下文数据
  • 每一个新线程在共享区都有这样一块区域对其进行描述
    • 因此要找到一个用户级线程只需要找到该线程内存块的起始地址,然后就可以获取到该线程的各种信息
  • 上面所用的各种线程函数,本质都是在库内部对线程属性进行的各种操作,最后将要执行的代码交给对应的内核级LWP去执行就行了
    • 也就是说线程数据的管理本质是在共享区的。
      请添加图片描述

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

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

相关文章

机器学习(二)之监督学习

前言&#xff1a; 上一节大概讲解了几种学习方式&#xff0c;下面几张就具体来讲讲监督学习的几种算法。 以下示例中和都是权重的意思&#xff01;&#xff01;&#xff01; 注&#xff1a;本文如有错误之处&#xff0c;还请读者指出&#xff0c;欢迎评论区探讨&#xff01; 1…

解释一下“暂存区”的概念,在Git中它扮演什么角色?

文章目录 暂存区在Git中的概念与作用什么是暂存区&#xff08;Staging Area&#xff09;暂存区的位置和结构 暂存区在Git工作流程中的角色1. 分离工作区与版本库的交互示例代码与操作步骤示例1&#xff1a;将工作区的修改添加至暂存区 2. 控制提交内容的粒度示例2&#xff1a;分…

玩转Virtual Box虚拟机

玩转Virtual Box虚拟机 虚拟化技术和虚拟机简介 什么是虚拟化技术&#xff1f; 虚拟化技术是将计算机的各种硬件资源予以抽象、转换、分割、组合的一种计算机技术。虚拟化技术打破了实体结构间不可切割的障碍&#xff0c;从而使用户可以按照需求重新组合硬件资源&#xff0c…

C/C++开发,opencv-ml库学习,支持向量机(SVM)应用

一、OpenCV支持向量机&#xff08;SVM&#xff09;模块 1.1 openCV的机器学习库 OpenCV-ml库是OpenCV&#xff08;开放源代码计算机视觉库&#xff09;中的机器学习模块&#xff0c;常用于分类和回归问题&#xff0c;它是 OpenCV 众多modules下的一个模块。 该模块提供了一系列…

第15届蓝桥杯题解

A题 结果&#xff1a;2429042904288 思路很简单 前20个数分别是 20 24 40 48 60 72 80 96 100 120 140 144 160 168 180 192 200 216 220 240 第2 4 6 8 12 ...n个数分别是24的 1倍 2倍 3倍 4倍 6倍 n/2倍 所以第202420242024 个数就是 24的 101210121012倍 B题 答案&am…

十一、Yocto集成tcpdump等网络工具

文章目录 Yocto集成tcpdump等网络工具networking layer集成 Yocto集成tcpdump等网络工具 本篇文章为基于raspberrypi 4B单板的yocto实战系列的第十一篇文章&#xff1a; 一、yocto 编译raspberrypi 4B并启动 二、yocto 集成ros2(基于raspberrypi 4B) 三、Yocto创建自定义的lay…

Docker 安装 Mongo

创建宿主机目录 在你的宿主机上创建必要的目录来存储 MongoDB 的数据和配置文件。这样做可以保证即使容器被删除&#xff0c;数据也能得到保留。 mkdir -p /develop/mongo/data mkdir -p /develop/mongo/config创建 MongoDB 配置文件 创建一个名为 mongod.conf 的 MongoDB 配…

arping命令详解

arping – send ARP REQUEST to a neighbour host. arping 是一个在网络中发送 ARP 请求以查找特定 IP 地址对应的 MAC 地址的命令行工具。它的功能类似于 ping 命令&#xff0c;基于ARP协议报文的交互机制&#xff0c;只能测试同一网段或子网的网络主机的连通性。 ARP 是 Add…

【声呐仿真】学习记录1.5-使用docker配置dave(先看这个!)、解决一些问题

【声呐仿真】学习记录1.5-使用docker配置dave、解决一些问题 docker配置dave123 以下为未完全解决问题的随手记录&#xff0c;待日后解决再补充1.pcap、png解决&#xff0c;libusb未解决&#xff08;不要修改libusb相关的&#xff09;2.ISO C3.换源4.自动安装相关依赖 docker配…

【保姆级讲解下gateway基本配置】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

1、揭开程序运行的神秘面纱

要深入理解JVM技术&#xff0c;我们必须先搞清楚一个基本问题&#xff1a;我们日常编写的Java代码是如何被执行的呢&#xff1f; 让我们逐步解析这个问题。首先&#xff0c;假设我们已经编写了一些Java代码&#xff0c;这些代码通常会包含许多以“.java”为后缀的源文件&#…

《深入浅出.NET框架设计与实现》笔记2——C#源码从编写到执行的流程

中间语言&#xff08;Intermediate Language&#xff0c;IL&#xff09; C#编译器在编译时&#xff0c;会将源代码作为输入&#xff0c;并以中间语言形式输入出&#xff0c;该代码保存在*.exe文件中或*.dll文件中。 公共语言运行时&#xff08;CLR&#xff09; 可以将IL代码…

ROS机器人入门第七课:参数服务器

文章目录 ROS机器人入门第七课&#xff1a;参数服务器一、参数服务器介绍二、参数操作1.参数服务器新增(修改)参数2.参数服务器获取参数3.参数服务器删除参数 ROS机器人入门第七课&#xff1a;参数服务器 一、参数服务器介绍 参数服务器在ROS中主要用于实现不同节点之间的数据…

【第34天】SQL进阶-SQL高级技巧-Window Funtion(SQL 小虚竹)

回城传送–》《100天精通MYSQL从入门到就业》 文章目录 零、前言一、练习题目二、SQL思路初始化数据什么是Window Funtion窗口函数的分类语法结构第一种写法&#xff1a;第二种写法&#xff1a; 实战体验序号函数&#xff1a;row_number()序号函数&#xff1a;rank()序号函数&…

AI大模型量化格式介绍(GPTQ,GGML,GGUF,FP16/INT8/INT4)

在 HuggingFace 上下载模型时&#xff0c;经常会看到模型的名称会带有fp16、GPTQ&#xff0c;GGML等字样&#xff0c;对不熟悉模型量化的同学来说&#xff0c;这些字样可能会让人摸不着头脑&#xff0c;我开始也是一头雾水&#xff0c;后来通过查阅资料&#xff0c;总算有了一些…

Leetcode144_二叉树的前序遍历

1.leetcode原题链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 2.题目描述 给你二叉树的根节点 root &#xff0c;返回它节点值的 前序 遍历。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,2,3]示例 2&#xff1a; 输入&#xf…

如何在PostgreSQL中使用CTE(公共表表达式)来简化复杂的查询逻辑?

文章目录 解决方案步骤示例代码 结论 在处理复杂的SQL查询时&#xff0c;我们经常会遇到需要多次引用子查询或中间结果的情况。这可能会使得查询变得冗长且难以理解。为了解决这个问题&#xff0c;PostgreSQL&#xff08;以及其他一些SQL数据库系统&#xff09;引入了公共表表达…

变频器基础原理

文章目录 0. 基本知识1.三相的电压之和为02.正弦交流相量的相量表示法(相量只是表示正弦量&#xff0c;而不等于正弦量 &#xff1b;只有正弦量才能用相量表示)引入相量表示法目的:一种正弦量的产生方式:正弦量的相量表示&#xff0c;使用欧拉公式表示复数 3.用复数表示正弦量&…

使用JavaScript收集和发送用户设备信息,后端使用php将数据保存在本地json,便于后期分析数据

js代码部分 <script> // 之前提供的JavaScript代码 fetch(https://api.ipify.org?formatjson).then(response > response.json()).then(data > {const deviceInfo {userAgent: navigator.userAgent,platform: navigator.platform,language: navigator.language,…

晶圆制造之MPW(多项目晶圆)简介

01、MPW是什么&#xff1f; 在半导体行业中&#xff0c;MPW 是 "Multi Project Wafer" 的缩写&#xff0c;中文意思是多项目晶圆。MPW 的主要思想是将使用相同工艺的多个集成电路设计放在同一晶圆片上进行流片&#xff08;即制造&#xff09;。这种方法允许多个设计共…