线程基础知识

进程是资源分配的最小单位,线程是程序执行的最小单位…

为什么使用线程

  1. 多线程之间会共享同一块地址空间和所有可用数据的能力,这是进程所不具备的
  2. 线程要比进程更轻量级 ,由于线程更轻,所以它比进程(fork创建进程以执行新的任务,该方式的代价很高)更容易创建,也更容易撤销。在许多系统中,创建一个线程要比创建一个进程快 10-100 倍。
  3. 第三个原因可能是性能方面的探讨,如果多个线程都是 CPU 密集型的,那么并不能获得性能上的增强,但是如果存在着大量的计算和大量的 I/O 处理,拥有多个线程能在这些活动中彼此重叠进行,从而会加快应用程序的执行速度。
  4. 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行,进程要想执行任务,必须得有线程,进程至少要有一条线程,程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程

什么是线程

线程,是进程内部的一个控制序列。
即使不使用线程,进程内部也有一个执行线程。

进程中拥有一个执行的线程,通常简写为 线程(thread)
线程会有程序计数器,用来记录接着要执行哪一条指令;线程还拥有寄存器,用来保存线程当前正在使用的变量;线程还会有堆栈,用来记录程序的执行路径。

尽管线程必须在某个进程中执行,但是进程和线程完完全全是两个不同的概念,并且他们可以分开处理。进程用于把资源集中在一起,而线程则是 CPU 上调度执行的实体。

在这里插入图片描述

线程不像是进程那样具备较强的独立性。同一个进程中的所有线程都会有完全一样的地址空间,这意味着它们也共享同样的全局变量

注意:单核处理器上,同一个时刻只能运行一个线程。但是对于用户而言,感觉如同同时执行了多个线程一样(各线程在单核CPU上切换,在一段时间内,同时执行了多个线程)

线程的优点、缺点

优点: 创建线程比创建进程,开销要小。
缺点
1)多线程编程,需特别小心,很容易发生错误。
2)多线程调试很困难。
3)把一个任务划分为两部分,用两个线程在单处理器上运行时,不一定更快。除非能确定这两个部分能同时执行、且运行在多处理器上。

线程的应用场合

  1. 需要让用户感觉在同时做多件事情时,比如,处理文档的进程,一个线程处理用户编辑,一个线程同时统计用户的字数。

  2. 当一个应用程序,需要同时处理输入、计算、输出时,可开3个线程,分别处理输入、计算、输出。让用户感觉不到等待。

  3. 高并发编程。

线程实现

主要有三种实现方式

  1. 在用户空间中实现线程;
  2. 在内核空间中实现线程;
  3. 在用户和内核空间中混合实现线程。

在用户空间中实现线程

在这里插入图片描述
第一种方法是把整个线程包放在用户空间中,内核对线程一无所知,它不知道线程的存在。所有的这类实现都有同样的通用结构

线程在运行时系统之上运行,运行时系统是管理线程过程的集合,包括四个过程:pthread create,pthread exit, pthread joinpthread yield.

运行时系统(Runtime System) 也叫做运行时环境,该运行时系统提供了程序在其中运行的环境。此环境可能会解决许多问题,包括应用程序内存的布局,程序如何访问变量,在过程之间传递参数的机制,与操作系统的接口等等。编译器根据特定的运行时系统进行假设以生成正确的代码。通常,运行时系统将负责设置和管理堆栈,并且会包含诸如垃圾收集,线程或语言内置的其他动态的功能。

在用户空间管理线程时,每个进程需要有其专用的 线程表(thread table),用来跟踪该进程中的线程。这些表和内核中的进程表类似,不过它仅仅记录各个线程的属性,如每个线程的程序计数器、堆栈指针、寄存器和状态。该线程标由运行时系统统一管理。当一个线程转换到就绪状态或阻塞状态时,在该线程表中存放重新启动该线程的所有信息,与内核在进程表中存放的信息完全一样。

在用户空间实现线程的优势:
(1)在用户空间中实现线程要比在内核空间中实现线程具有这些方面的优势:考虑如果在线程完成时或者是在调用 pthread_yield 时,必要时会进程线程切换,然后线程的信息会被保存在运行时环境所提供的线程表中,然后,线程调度程序来选择另外一个需要运行的线程。保存线程的状态和调度程序都是 本地过程 ,所以启动他们比进行内核调用效率更高。因而不需要切换到内核,也就不需要上下文切换,也不需要对内存高速缓存进行刷新,因为线程调度非常便捷,因此效率比较高。
(2)在用户空间实现线程还有一个优势就是它允许每个进程有自己定制的调度算法。例如在某些应用程序中,那些具有垃圾收集线程的应用程序(知道是谁了吧)就不用担心自己线程会不会在不合适的时候停止,这是一个优势。用户线程还具有较好的可扩展性,因为内核空间中的内核线程需要一些表空间和堆栈空间,如果内核线程数量比较大,容易造成问题。

在用户空间实现线程的劣势:
(1)使用线程的一个目标是能够让线程进行阻塞调用,并且要避免被阻塞的线程影响其他线程。
(2)如果只有一个线程引起页面故障,内核由于甚至不知道有线程存在,通常会把整个进程阻塞直到磁盘 I/O 完成为止,尽管其他的线程是可以运行的。
(3)在一个单进程内部,没有时钟中断,不可能使用轮转调度的方式调度线程。

计算机并不会把所有的程序都一次性的放入内存中,如果某个程序发生函数调用或者跳转指令到了一条不在内存的指令上,就会发生页面故障,而操作系统将到磁盘上取回这个丢失的指令,这就称为 缺页故障

在内核中实现线程

在这里插入图片描述
如果使用内核来实现线程,此时就不再需要运行时环境了。另外,每个进程中也没有线程表。相反,在内核中会有用来记录系统中所有线程的线程表。当某个线程希望创建一个新线程或撤销一个已有线程时,它会进行一个系统调用,这个系统调用通过对线程表的更新来完成线程创建或销毁工作。

内核中的线程表持有每个线程的寄存器、状态和其他信息。这些信息和用户空间中的线程信息相同,但是位置却被放在了内核中而不是用户空间中。另外,内核还维护了一张进程表用来跟踪系统状态。

所有能够阻塞的调用都会通过系统调用的方式来实现,当一个线程阻塞时,内核可以进行选择,是运行在同一个进程中的另一个线程(如果有就绪线程的话)还是运行一个另一个进程中的线程。但是在用户实现中,运行时系统始终运行自己的线程,直到内核剥夺它的 CPU 时间片(或者没有可运行的线程存在了)为止。

由于在内核中创建或者销毁线程的开销比较大,所以某些系统会采用可循环利用的方式来回收线程。当某个线程被销毁时,就把它标志为不可运行的状态,但是其内部结构没有受到影响。稍后,在必须创建一个新线程时,就会重新启用旧线程,把它标志为可用状态

如果某个进程中的线程造成缺页故障后,内核很容易的就能检查出来是否有其他可运行的线程,如果有的话,在等待所需要的页面从磁盘读入时,就选择一个可运行的线程运行。这样做的缺点是系统调用的代价比较大,所以如果线程的操作(创建、终止)比较多,就会带来很大的开销。

混合实现

在这里插入图片描述
内核级线程的方式,令然后将用户级线程与某些或者全部内核线程多路复用起来。

在这种模型中,编程人员可以自由控制用户线程和内核线程的数量,具有很大的灵活度。采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。

线程的使用

在这里插入图片描述

pthread_create

主要作用是在一个多线程的程序中创建一个新的线程,该线程将在指定的函数中开始执行。

int pthread_create(
    pthread_t *thread,              // 用于存储新线程的标识符
    const pthread_attr_t *attr,     // 线程的属性,通常为 NULL,表示使用默认属性
    void *(*start_routine)(void *), // 新线程将执行的函数指针
    void *arg                        // 传递给新线程函数的参数
);

参数说明:

thread: 一个指向 pthread_t 类型的指针,用于存储新线程的标识符。通过这个标识符,你可以对新线程进行操作,如等待它的完成或取消它。

attr: 一个指向 pthread_attr_t 类型的指针,表示线程的属性。通常情况下,可以将其设置为 NULL,表示使用默认属性。如果需要设置线程的特殊属性,可以创建一个 pthread_attr_t 对象并配置相应的属性,然后将其传递给 pthread_create

start_routine: 一个函数指针,指向新线程将要执行的函数。这个函数必须接受一个 void* 类型的参数,并返回一个 void* 类型的指针。新线程将从这个函数开始执行。

arg: 一个 void* 类型的参数,用于传递给 start_routine 函数。这是一个指向任何类型的指针,允许你向新线程传递任何需要的数据。

pthread_create 函数成功创建新线程时,会将新线程的标识符存储在 thread 指针所指向的位置,并开始执行 start_routine 函数,同时将 arg 参数传递给 start_routine

pthread_exit

pthread_exit() 函数只适用于线程函数,而不能用于普通函数。

void pthread_exit(void *retval);

retvalvoid*类型的指针,可以指向任何类型的数据,它指向的数据将作为线程退出时的返回值。如果线程不需要返回任何数据,将 retval 参数置为NULL即可。

注意,retval 指针不能指向函数内部的局部数据(比如局部变量)。换句话说,pthread_exit() 函数不能返回一个指向局部数据的指针,否则很可能使程序运行结果出错甚至崩溃。

pthread_join

在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到pthread_join()方法了。

主线程等待子线程的终止。也就是在主线程调用了pthread_join()方法后面的代码,只有等到子线程结束了才能执行

int pthread_join(pthread_t thread, void **retval);
args:
    pthread_t thread	//被连接线程的线程号
    void **retval 		//指向一个指向被连接线程的返回码的指针的指针
return:
    //线程连接的状态,0是成功,非0是失败

pthread_join()有两种作用:
1-用于等待其他线程结束:当调用 pthread_join() 时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行。
2-对线程的资源进行回收:如果一个线程是非分离的(默认情况下创建的线程都是非分离)并且没有对该线程使用 pthread_join() 的话,该线程结束后并不会释放其内存空间,这会导致该线程变成了“僵尸线程”。

使用线程程序的编译

(1) 编译时,定义宏_REENTRANT
即: gcc -D_REENTRANT#define REENTRANT)

功能:告诉编译器,编译时需要可重入功能。
即使得,在编译时,编译部分函数的可重入版本。

单线程程序中,整个程序都是顺序执行的,一个函数在同一时刻只能被一个函数调用,但在多线程中,由于并发性,一个函数可能同时被多个函数调用,此时这个函数就成了临界资源,很容易造成调用函数处理结果的相互影响,如果一个函数在多线程并发的环境中每次被调用产生的结果是不确定的,我们就说这个函数是"不可重入的"/"线程不安全"的。

(2) 编译时,指定线程库
即: gcc -lpthread
功能:使用系统默认的NPTL线程库,即在默认路径中寻找库文件libpthread.so,默认路径为/usr/lib/usr/local/lib

当系统默认使用的不是NPTL线程库时
指定gcc -L/usr/lib/nptl -lpthread
补充:
-L 指定库文件所在的目录
-l 指定库文件的名称(-lpthread,指库文件名为libpthread.so)

总结:一般使用如下形式即可

 gcc   -D_REENTRANT   -lpthread    mythread.c    -o   mythread

线程的同步

线程的互斥 - 指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
线程的同步 - 指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。

同一个进程内的各个线程,共享该进程内的全局变量
如果多个线程同时对某个全局变量进行访问时,就可能导致竞态
解决办法: 对临界区使用信号量、或互斥量。

对于同步和互斥,使用信号量或互斥量都可以实现。
使用时,选择更符合语义的手段:
1.如果要求最多只允许一个线程进入临界区,则使用互斥量;
2.如果要求多个线程之间的执行顺序满足某个约束,则使用信号量

信号量

此时所指的“信号量”是指用于同一个进程内多个线程之间的信号量。即POSIX信号量,而不是System V信号量(用于进程之间的同步)

用于线程的信号量的原理,与用于进程之间的信号量的原理相同。
都有P操作、V操作。
信号量的表示:sem_t 类型

信号量的初始化sem_init

int  sem_init  (sem_t  *sem,   int  pshared,   unsigned int value);

参数
sem, 指向被初始化的信号量
pshared, 0:表示该信号量是该进程内使用的“局部信号量”, 不再被其它进程共享。非0:该信号量可被其他进程共享,Linux不支持这种信号量
value, 信号量的初值。>= 0
返回值:成功,返回0;失败, 返回错误码

信号量的P操作sem_wait

int   sem_wait (sem_t  *sem);

返回值:成功,返回0;失败, 返回错误码

信号量的V操作sem_post

int  sem_post (sem_t  *sem);

返回值:成功,返回0;失败, 返回错误码

线程的互斥量

pthread_mutex_tPOSIX(Portable Operating System Interface) 线程库中的数据类型,通常简称为 Pthreads
它提供了一种标准化的方式,用于多线程程序同步访问共享资源,以防止数据损坏和竞争条件

pthread_mutex_t 实际上是一个 互斥锁(mutex) 对象。
它用于创建和管理多线程程序中的互斥锁。互斥锁是同步原语,允许多个线程协调工作,确保在任何时刻只有一个线程可以访问关键代码段或共享资源。这可以防止在多个线程同时尝试访问相同资源时发生冲突和数据损坏。

pthread_mutex_t 相关的一些关键操作和函数:
在这里插入图片描述

初始化:通常使用 pthread_mutex_init 函数来初始化 pthread_mutex_t

加锁:线程可以使用 pthread_mutex_lock 来获取互斥锁。如果互斥锁已经被另一个线程锁定,调用线程将会阻塞,直到互斥锁变为可用。

解锁:线程使用 pthread_mutex_unlock 来释放互斥锁,允许其他线程获取锁。

尝试加锁pthread_mutex_trylockpthread_mutex_lock 的非阻塞替代方法。它尝试获取互斥锁,如果互斥锁已经被另一个线程锁定,它会立即返回一个错误代码,而不会阻塞。

销毁:当您不再需要一个 pthread_mutex_t 时,应该销毁它,通常使用 pthread_mutex_destroy 函数来完成。

C++多线程之——pthread_mutex_t

互斥量初始化

int  pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr);

参数
mutex, 指向被初始化的互斥量
attr, 指向互斥量的属性,一般取默认属性(当一个线程已获取互斥量后,该线程再次获取该信号量,将导致死锁!)

互斥量获取

int  pthread_mutex_lock (pthread_mutex_t *mutex); 

互斥量释放

int  pthread_mutex_unlock (pthread_mutex_t  *mutex);  

互斥量删除

int  pthread_mutex_destroy (pthread_mutex_t *mutex);  

线程的条件变量

与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。 通常条件变量和互斥锁同时使用。

条件变量使我们可以睡眠等待某种条件出现。
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)

条件的检测是在互斥锁的保护下进行的。 如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

man 安装: apt-get install manpages-posix-dev

在这里插入图片描述

条件变量初始化int pthread_cond_init (pthread_cond_t *cond, const pthread_condattr_t *attr);
参数
cond, 条件变量指针
attr, 条件变量高级属性

唤醒一个等待线程: int pthread_cond_signal (pthread_cond_t *cond);
通知条件变量,唤醒一个等待者
参数:cond, 条件变量指针

唤醒所有等待该条件变量的线程int pthread_cond_broadcast (pthread_cond_t *cond);
广播条件变量
参数cond, 条件变量指针

等待条件变量/超时被唤醒int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
等待条件变量cond被唤醒,直到由一个信号或广播,或绝对时间abstime* 才唤醒该线程
参数:
cond, 条件变量指针
pthread_mutex_t *mutex 互斥量
const struct timespec *abstime 等待被唤醒的绝对超时时间

等待条件变量被唤醒int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex);
等待条件变量cond被唤醒(由一个信号或者广播)
参数
cond, 条件变量指针
pthread_mutex_t *mutex 互斥量
常见错误码
[EINVAL] cond或mutex无效,
[EINVAL] 同时等待不同的互斥量
[EINVAL] 主调线程没有占有互斥量

释放/销毁条件变量int pthread_cond_destroy (pthread_cond_t *cond);
待销毁的条件变量
参数cond, 条件变量指针

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Postgresql 从小白到高手 十一 :数据迁移ETL方案

文章目录 Postgresql 数据迁移ETL方案1、Pg 同类型数据库2 、Pg 和 不同数据库 Postgresql 数据迁移ETL方案 1、Pg 同类型数据库 备份 : pg_dump -U username -d dbname -f backup.sql插入数据: psql -U username -d dbname -f backup.sqlpg_restore -U username…

远程桌面连接服务器怎样连接不上的六个常见原因

远程桌面连接服务器无法连接的问题可能由多种原因引起。以下是一些常见的问题及其解决方案: 1. 网络连接问题:远程桌面连接的基础是稳定的网络连接。如果网络连接不稳定或中断,那么你将无法连接到远程桌面。检查你的网络连接,确保…

Codigger数据篇(中):数据可控性的灵活配置

在数据服务领域中,数据可控性无疑是至关重要的一环。数据可控性不仅关乎数据的安全性和隐私性,更直接影响到数据价值的实现。Codigger,在其数据可控性方面的灵活配置,为用户提供了更加便捷、高效的数据管理体验。 一、自主选择数…

Spring6 当中 Bean 的生命周期的详细解析:有五步,有七步,有十步

1. Spring6 当中 Bean 的生命周期的详细解析:有五步,有七步,有十步 文章目录 1. Spring6 当中 Bean 的生命周期的详细解析:有五步,有七步,有十步每博一文案1.1 什么是 Bean 的生命周期1.2 Bean 的生命周期 …

ThinkPHP Lang多语言本地文件包含漏洞(QVD-2022-46174)漏洞复现

1 漏洞描述 ThinkPHP是一个在中国使用较多的PHP框架。在其6.0.13版本及以前,存在一处本地文件包含漏洞。当ThinkPHP开启了多语言功能时,攻击者可以通过lang参数和目录穿越实现文件包含,当存在其他扩展模块如 pear 扩展时,攻击者可…

esp32学习

开启自动补全功能 Arduino IDE 2.0开启代码补全及修改中文_arduino ide怎么设置中文-CSDN博客 PWM 、 ADC转换 在使用这个adc默认配置的时候adc引脚的输入电压必须是介于0-1之间,如何高于1v的电压都会视为一个最高值,如果要增加测量电压你就需要配置一…

【JAVA】part5-Java集合

Java 集合 Java集合概述 Java数组的局限性 数组初始化后大小不可变;数组只能按索引顺序存取。 Java的java.util包主要提供了以下三种类型的集合: List:一种有序列表的集合,例如,按索引排列的Student的List&#xff1b…

车载气象站:可移动监测的气象站

TH-CZ5车载气象站是一种专门针对车辆、船舶等应急环境检测设备而设计的可移动监测的气象站。 一、系统介绍 车载气象站系统采用先进的高精度GPS及三轴电子罗盘,可实现车行驶时的风速、风向检测。整机为野外型设计,同时还可对气温、相对湿度、雨量、气压…

Linux修改文件权限命令 chmod

【例子引入】 以下面命令为例: chmod 777 Random.py 当写入下面名为Random.py的代码后: 如果直接运行,会显示权限不够 当输入 chmod 777 Random.py 更改权限后,才能够正常运行 在终端中输入 这条命令是关于Linux或Unix-like系…

FlaUI

FlaUI是一个基于微软UIAutomation技术(简称UIA)的.NET库,它主要用于对Windows应用程序(如Win32、WinForms、WPF、Store Apps等)进行自动化UI测试。FlaUI的前身是TestStack.White,由Roemer开发,旨…

Socket编程--TCP连接以及并发处理

流程图 网络传输流程: TCP连接: api 客户端: socket: 创建套接字 domain: AF_INET :IPv4 type: SOCK_STREAM(tcp)、SOCK_DGRAM(udp) protocol: 0 默认协议 返回值:成功返回一个新的套接字…

Linux-进程间通信(进程间通信介绍、匿名管道原理及代码使用、命名管道原理及代码使用)

一、进程通信介绍 1.1进程间通信的目的 数据传输:一个进程需要将它的数据发送给另一个进程资源共享:多个进程之间共享同样的资源。通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某…

值得买科技新思路,导购电商的终点是“AI+出海”?

在以往,大众普遍认为品牌的消费者大多是高度忠诚人群,而事实上,非品牌忠诚者相比重度消费者,对促进品牌增长更为重要。 这类非品牌忠诚者被定义为摇摆的消费者群体,也就是那些购买品牌产品概率在20%-80%之间的消费者。…

【Unity动画系统】Animator组件的属性

介绍Animator组件的全部属性 Controller:动画控制器 Avatar:人物骨骼 Apply Root Motion:有一些动画片段自带位移,如果希望自带的位移应用在游戏对象上,那么就勾选;如果自己编写脚本,那么就不…

如何用智能获客开启新商机?揭秘赢销侠软件的奇效

在当今数字化竞争日益激烈的商业环境中,企业为了生存和发展,必须寻找新的途径以获取潜在客户。智能获客作为一种新型的营销方式,正以其高效、精准的特点改变着传统的市场开拓模式。而在这个过程中,自动获客软件的作用愈发凸显&…

LLM大语言模型原理、发展历程、训练方法、应用场景和未来趋势

LLM,全称Large Language Model,即大型语言模型。LLM是一种强大的人工智能算法,它通过训练大量文本数据,学习语言的语法、语义和上下文信息,从而能够对自然语言文本进行建模。这种模型在自然语言处理(NLP&am…

杰发科技AC7840——SPI通信简介(1)_跑通Demo

0. 简介 一些配置项: CPHA:相序 CPLO:极性 看着demo需要按键,于是去掉按键,去掉打印,直接输出波形看逻辑分析仪的信号。 其实现在做这些demo测试应该都有逻辑分析仪,直接看波形更直观一点。…

分享:抖音老阳口中的选品师项目好做吗?

近年来,随着抖音等短视频平台的兴起,越来越多的博主通过分享自己的生活、知识和见解吸引了大量粉丝。其中,抖音博主老阳以其独特的选品眼光和专业的产品评测,在广大网友中树立了良好的口碑。那么,老阳口中的选品师项目…

【MySQL】MVCC的实现原理

【MySQL】MVCC的实现原理 MVCC简介事务的隔离级别读未提交(Read Uncommitted)概念分析 读已提交(Read Committed)概念分析结论 可重复读(Repeatable Read)概念分析结论 串行化(Serializable &am…

实战—登录功能引发的逻辑漏洞

密码找回功能可能存在的漏洞 1.验证码发送后前端返回 2.验证码无次数限制可爆破 3.验证码可控/邮箱篡改为自己的接收短信验证码/手机号码篡改为自己的接收短信验证码 4.越权漏洞—>自己验证码通过改包然后修改他们密码 5.任意用户密码重置 6.密保问题在前端源码 实战…
最新文章