【Linux学习】多线程——互斥 | 线程安全

news2024/9/29 1:30:59

🐱作者:一只大喵咪1201
🐱专栏:《Linux学习》
🔥格言:你只管努力,剩下的交给时间!
图

互斥 | 线程安全

  • 🥩线程不安全
    • 🍚线程不安全的原因
  • 🥩线程互斥
    • 🍚加锁
    • 🍚锁的本质
  • 🥩锁的封装
  • 🥩可重入VS线程安全
  • 🥩死锁
    • 🍚死锁的必要条件
  • 🥩总结

🥩线程不安全

从一个例子入手:

图

  • 全局变量tickets表示票的数量。
  • 主线程中创建了4个新线程,代表4个用户去抢票。
  • 每个新线程在抢到票后将票数减一,并且打印出票的数量。
  • 当票被抢完以后,线程退出,不再进行抢票。

图
运行以后,发现出现了负数票,这不合理,票抢完就应该停止了,包括我们的代码逻辑都是这样写的,但是此时就出现了这种情况。

  • 上面现象的原因是发生了线程不安全问题

如何产生线程不安全现象:

上面现象故意弄出来的,涉及到了线程调度,利用了线程调度的特性造出了一个这样的现象。要想出现上面的现象,就需要:

  • 尽可能让多个线程交叉执行。
  • 多个线程交叉执行的本质:就是让调度器尽可能的频繁发生线程调度与切换。

虽然看起来是多个线程在同时运行,但这是由于CPU运行速度太快导致的,实际上,CPU是一个线程一个线程执行的。现在就是要让CPU频繁调度,不停的切换线程,一个线程还没有执行完就再执行下一个,每个线程都执行一点,这样交叉执行。

当一个线程进行延时的时候,CPU并不会等它,而是会将它放在等待队列里,然后去执行另一个线程,等延时线程醒来以后才会接着执行。

  • 线程在时间片到来,更高优先级线程到来,线程等待的时候会发生线程切换。
  • 线程是在从内核态转换成用户态的时候检测是否达到线程切换的条件的。
  • 线程检测是否切换是以内核态的身份去检测的,执行的是3~4G内核空间中的代码,本质上是操作系统在检测。

产生线程不安全现象的原因:

假设tickets已经只剩一张了,即全局变量tickets = 1。

图
主线程创建好4个新线程以后,4个新线程便开始执行了,在执行到延时的时候,新线程就会被放在等待队列里。

看CPU及内核:

if判断的本质逻辑:

  • 从内存中读取数据到CPU寄存器。
  • 进行判断。

图

  • 在线程user1执行到if判断时,CPU从内存中将tickets变量中的数据1拿到了CPU的寄存器ebx中。
  • CPU进行判断后,发现符合大于0的条件。

当线程user1符合条件继续向下执行延时代码时,CPU将线程user1切走了,换上了user2。

图

  • 在线程user1被切走的时候,它的上下文数据也会被切走。
  • 所以ebx寄存器中的1也会跟着user1的PCB被切走。

user2被调度时仍然重复user1的过程,执行延时被切走,再换上user3,以此类推,直到user4被切走。

  • 四个线程都拿到了tickets=1,所以符合条件,都能向下执行。
  • 当user4被挂起后,user1差不多就该醒来了。

user1唤醒以后接着被切走的位置继续执行:

图

执行tickets - - 的本质:

  • 从内存中读取数据到CPU的寄存器
  • 更改数据
  • 写回数据到内存中

虽然C/C++代码只有一条语句,但是汇编后至少有3条语句。

user1执行tickets–以后,抢票成功了,并且将抢票后的tickets=0写回到了内存中。

此时user2醒来了,同样接着它被切走的位置继续执行,此时user2回来后认为tickets=1,所以就向下执行了:

图
当执行tickets减减时,仍然需要三步:

  • 从内存中读取tickets=0到CPU寄存器ebx中。
  • 修改值,从0变成-1。
  • 将-1写回内存中。

当user2执行完后,user3和user4醒来同样继续向下执行,重复上面的过程,仍然对tickets减一,所以导致结果不合理。

🍚线程不安全的原因

只存在两个线程,对全局变量tickets仅作减减操作:

图

  • 线程A先被CPU调度,进行减减操作。
  • 从内存中将tickets=1000取到寄存器ebx中。
  • 进行减减操作,tickets变成了999。
  • 在执行第三步写回数据之前,线程A被切走了。

线程A切走的同时,它的上下文,也就是tickets=999也被切走了。

图

  • 线程B此时被调度,线程A在等待队列。
  • 线程B先从内存中读取tickets = 1000到寄存器ebx中。
  • 进行减减操作。
  • 将减减后的值写回到内存中。
  • 线程B将减减操作完整的执行了很多遍,直到tickets=200时才被切下去。

线程B被切走以后,线程A又接着被调度。

图

  • 线程A接着被切走的位置开始执行,也就是执行减减的第三步操作—写回。
  • 线程A被调度后,先恢复上下文,将被切走时的tickets=999恢复到了ebx寄存器中。
  • 然后执行第三步,将tickets=999写回到了内存中。

线程B辛辛苦苦将tickets从1000减到了200,线程A重新被调度后,直接将tickets又从200写回到了999。

上面这种现象被叫做数据不一致问题

  • 导致数据不一致问题的原因:共享资源没有被保护,多线程对该资源进行了交叉访问。

而解决数据不一致问题的办法就是对共享资源加锁。

🥩线程互斥

基本概念:

  • 临界资源:多个执行流进行安全访问的共享资源。

上面现象中的tickets很显然就不是临界资源,因为多线程对它的访问并不安全,存在数据不一致问题。

  • 临界区:多个执行流中,访问临界资源的代码。

假设上面例子中的是临界资源,那么每个线程都存在一部分临界区,就是对tickets进行判断,打印,减减部分的代码。多个线程中的这部分代码属于临界区。

  • 互斥:让多个线程串行访问共享资源,任何时候只有一个执行流在访问共享资源。

上面例子中如果多个线程能够串行访问tickets,而不是交叉访问,也不会产生数据不一致问题。而让共享资源变成临界资源就是为了实现互斥,也就是让多个线程串行访问原本的共享资源。

  • 原子性:对一个资源进行访问的时候,要么不做,要么就做完。

在C/C++中的减减和加加操作,看似是一句代码,但是对应着三条汇编指令,上面例子中,线程A在执行第三步之前被切走了,导致减减操作没有完成,这种行为就不具有原子性,因为对共享资源的操作没有做完。

  • 对一个资源进行操作,如果只用一条汇编就能完成,那么就具有原子性,反正就不是原子的。

这是当前的一种理解,这种理解只能算原子性中的一个子集,是为了方便表述。

🍚加锁

要想解决多线程的数据不一致问题,就需要做到以下几点:

  • 代码必须要有互斥行为,当一个线程进入临界区执行代码时,不允许其他线程进入该临界区。
  • 如果有多个线程同时请求执行临界区代码,并且临界区没有线程在执行代码,那么只允许一个线程进入该临界区。
  • 如果线程不在临界区中执行代码,那么该线程不能阻止其他线程进入临界区。

要做到上面三点,只需要一把锁就可以,持有锁的线程才能进入临界区中执行代码,并且其他线程无法进入该临界区。

  • 锁:就是互斥量,也叫互斥锁。

加锁可以让共享资源临界资源化,从而保护共享资源的安全,让多个线程串行访问共享资源。

和锁相关的系统调用:

pthread_mutex_t lock;

和创建线程一样,锁也需要创建,POSIX提供了锁的变量类型,如上面代码所示,其中mutext是互斥量的意思。

初始化锁:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
  • 形参1:创建的互斥锁指针
  • 形参2:直到锁的属性,一般情况下设为nullptr
  • 返回值:初始化成功返回0,失败返回错误码
  • 作用:将创建的锁初始化。

销毁锁:

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 形参:创建的互斥锁指针
  • 返回值:销毁成功返回0,失败返回错误码
  • 作用:当锁使用完后,必须进行销毁

全局或者静态锁初始化:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

如果锁是全局的或者被static修饰的静态锁,只需要使用上面语句初始化锁即可。

加锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 形参:创建的互斥锁指针
  • 返回值:加锁成功返回0,失败返回错误码
  • 作用:给临界区加锁,让多线程串行访问临界资源

解锁:

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 形参:创建的互斥锁指针
  • 返回值:解锁成功返回0,失败返回错误码
  • 作用:解锁,让多线程恢复并发执行

锁其实起一个区间划分的作用,在加锁和解锁之间的代码就是临界区,多个执行流只能串行执行临界区代码,从而保护公共资源,使之成为临界资源。

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(lock);
//临界区
//...
pthread_mutex_unlock(lock);

加锁和解锁两句代码圈定了临界区的范围。

现在将抢票代码加上锁,看看是否还会出现多线程数据不一致问题:

图

在主线程中创建一个互斥锁,并且初始化,在所有新线程等待成功后将锁释放。

  • 但是此时的锁是存在于主线程的栈结构中,需要让所有新线程看到这把锁。

图

在线程数据结构体中再增加一个锁指针,此时所有线程就都能看到这把锁了。

图
在新线程中对临界区加锁和解锁,让所有线程串行执行临界区中代码。

  • 解锁不能放在else的代码块后面,防止break出循环,但是没有解锁。
  • 在else的break前也要有解锁,防止if条件不满足直接跳出循环没有解锁。

图
此时抢票的结果是正常了,最终抢到1结束,符合我们的预期。

  • 抢票的速度比以前慢了好多。
  • 加锁和解锁的过程是多个线程串行执行的,并且临界区的代码也是串行执行的,所以速度就变慢了。
  • 只有user3在抢票,其他线程没有抢。
  • 锁只规定了互斥访问,并没有规定必须让谁先执行。
  • 锁是让多个执行流进行竞争的结果。

只有user3在执行,说明user3的竞争能力强,别的线程抢不过它。因为现在的抢票逻辑是抢到票以后立马释放然后就又立马申请锁了,所以之前持有锁的线程更加容易再次申请到锁。

实际上,抢票成功后不可能立刻再去抢,还需要做一些工作,比如给用户打印订单等等。

图
在抢票成功后延时1000微秒,代表线程做的后续工作。

图
此时就成了多个线程在一起抢票。

  • 当一个线程从临界区中出来并且释放锁后,执行后续任务时,其他线程才有能力去竞争锁。

图
加锁后的代码结构上如上图所示。

  • 加锁时,一定要保证临界区的粒度非常小。将那些不是必须放在临界区中的代码放在临界区外。
  • 加锁是程序员行为,要加锁就所有线程都加锁,否则就起不到保护共享资源的效果。

🍚锁的本质

如何看待锁?

  • 在上面代码中,一个锁必须让所有线程都看到,所以锁本身就是一个共享资源。

既然是共享资源,锁也必须是安全的,那么是谁来保证锁的安全性呢?

  • 锁是通过加锁和解锁是原子的来保证自身的安全的。

一个线程,如果申请成功锁,那么它就会继续向下执行,如果暂时申请不成功呢?

图
如上图代码所示,一个线程连续两次申请一个锁。

图
图

此时代码就被阻塞住了,线程和进程都是存在的。

  • 一个锁只能被申请一次,只有锁被释放后才能再次申请。
  • 当一个线程申请锁暂时失败以后,就会阻塞不动。

图
又多个线程在执行这部分代码。

  • 当一个线程申请锁成功,进入临界区访问临界资源,其他线程要想进入临界区只能阻塞等待,等锁释放。
  • 当一个线程申请锁成功,进入临界区访问临界资源,同样是能被切走的,而且该线程是抱着锁走的,其他线程仍然无法申请锁成功。

操作系统内部并不存在锁的概念,所以调度器在调度轻量级进程的时候并不会考虑是否有锁。

所以站在其他线程的角度,锁只有两种状态:

  • 申请锁前
  • 申请锁后

站在其他线程的角度,看到当前持有锁的过程就是原子的。

加锁解锁的原理:

经过上面的例子,我们认识到一个事实,c/c++中加加和减减的操作并不是原子的,所以会导致多线程数据不一致的问题。

  • 而为了能让加锁过程是原子的,在大多数体系结构了,都提供了swap或者xchange汇编指令,通过一条汇编指令来保证加锁的原子性。

加锁解锁的伪代码:

lock:
	movb %al, $0
	xchange %al, mutex
	if(al寄存器的内容 > 0)
	{
		return 0;
	}
	else
	{
		挂起等待;	
	}
	goto lock;

unlock:
	movb mutex, $1
	唤醒等待mutex的线程;
	return 0;

加锁过程中,xchange是原子的,可以保证锁的安全。

假设现在有两个线程,ThreadA和ThreadB:

图

线程A在执行,线程B在等待,线程A加锁时的第一步就是将0写入到al寄存器中。

  • 在执行完第一条汇编后,线程A是可以被切走的,而且在切走的时候会将它的上下文,也就是al中的0带走。
  • 这一步的本质就是将0写入到线程A的上下文中。

图
线程A在执行第二步的时候,直接将内存中mutex中的数据交换到了al寄存器中。

  • 在执行完第二步的时候,线程A同样可以被切走,而且是带着上下文走的,也就是会将al中的mutex带走。
  • 交换的本质就是将锁交换到线程A的上下文中。

假设现在线程A被切走了,而且带走了它的上下文mutex:

图
线程B在执行的时候,先第一步给寄存器al写0,然后执行第二步交换锁和al中的值。

  • 此时al中的值虽然交换了,但是仍然是0,根据上面的伪代码,if条件不成立,所以将线程B挂起等待了。

此时操作系统就会又将线程A调回来继续执行:

图
线程A做的第一件事情就是恢复上下文,将锁恢复到al寄存器中。

  • 线程A执行下一步时,if条件成了,所以该线程就申请锁成功了。

经过上面过程的描述,我们发现,锁只能被一个线程持有,而且由于xchange汇编只有一条指令,即使申请锁的过程被切走也不怕。

  • 一旦一个线程通过xchage拿到了锁,即使它被切走,也是抱着锁走的,其他线程是无法拿到锁的,只有等它将锁释放。

只有持有锁的线程才能执行下去,锁相当于一张入场卷

这样来看,释放锁的过程其实对原子性的要求并没有那么高,因为释放锁的线程必定是持有锁的线程,不持有锁的线程都不会执行到这里,都在阻塞等待。

图

  • 线程A在解锁时,仅是将内存中存放锁的变量写为1,此时其他线程在xchange以后就可以通过if条件判断,申请锁了。

虽然解锁对原子性的要求不是很必要,但是在设计上还是要设计成原子的,可以看到,解锁也是只通过一条汇编就搞定了。

提示:上图中锁中的变量1仅仅是表示锁存在,并不是真正意义上的数字1。

🥩锁的封装

为了更好的使用C++,像封装线程那样,将加锁也封装成一个小组件,方便我们后面使用。

Mutex.hpp:

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

class Mutex
{
public:
    Mutex(pthread_mutex_t* lock_p = nullptr):_lock_p(lock_p)
    {}
    void lock()
    {
        pthread_mutex_lock(_lock_p);//加锁
    }
    void unlock()
    {
        pthread_mutex_unlock(_lock_p);//解锁
    }
private:
    pthread_mutex_t* _lock_p;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t* lock_p):_mutex(lock_p)
    {
        _mutex.lock();//构造函数内加锁
    }
    ~LockGuard()
    {
        _mutex.unlock();//析构函数内解锁
    }
private:
    Mutex _mutex;
};

只需要创建一个LockGurd对象就可以进行加锁,需要传入锁的地址,在LockGuard对象生命周期结束的时候,会自动释放锁。

图

  • 创建一个全局的锁,就不用使用pthread_mutex_init取初始化,也不用使用pthread_mutex_destroy来销毁锁了,直接使用就行。

在临界区加锁,执行完临界区代码后解锁。

  • 将临界区放在一个代码块中,此时LockGuard的生命周期就是这个代码块。
  • 创建LockGuard对象时在构造函数中自动加锁,出作用域时析构函数自动解锁。

图
使用封装的加锁小组将,抢票的结果和我们之前直接用系统调用加锁是一样的。

  • 上面这种加锁的风格被称为RAII加锁

🥩可重入VS线程安全

重入:

  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。

之前在信号部分就提到过重入,进程在执行一个函数,收到某个信号在处理信号时又调用了这个函数。今天在多线程这里,理解重入更加容易,我们上面写的多线程代码都是重入的。

  • 可重入和不可重入:一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

常见可重入情况:

  • 不使用全局变量或静态变量。
  • 不使用用malloc或者new开辟出的空间。
  • 不返回静态或全局数据,所有数据都有函数的调用者提供。

常见不可重入情况:

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的函数。
  • 可重入函数体内使用了静态的数据结构。
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

总的来说,一个函数中如果使用了全局数据,或者静态数据,以及堆区上的数据,就是不可重入的,反之就是可重入的。

线程安全:

  • 多个线程并发同一段代码时,不会出现不同的结果(数据不一致)。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

互斥锁就是让不安全的线程变安全,也就是前面我们所学习的内容。

常见线程安全情况:

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
  • 类或者接口对于线程来说都是原子操作。
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性。

多线程共同执行的代码段中,如果有全局变量或者静态变量并且没有保护,那么就是线程不安全的。

常见线程不安全情况:

  • 不保护共享变量的函数。
  • 函数状态随着被调用,状态发生变化的函数。
  • 返回指向静态变量指针的函数。

可重入与线程安全的联系:

多线程是通过调用函数来实现的,所以线程安全和重入就存在一些联系:

  • 函数是可重入的,那就是线程安全的,因为没有全局或者静态变量,不会产生数据不一致问题。
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。出发对不可重入函数的全局变量进行保护。
  • 如果一个函数中有全局变量并且没有保护,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全的区别:

可重入和线程安全是不同的两个东西,但是又存在一定的交集。

  • 可重入说的是函数。
  • 线程安全说的是线程。
  • 可重入函数是线程安全函数的一种,因为不存在全局或者静态变量。
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。因为线程安全的情况可能是对全局变量进行了保护(加了锁)。

由于线程可以加锁,所以说线程安全的情况比可重入要多。

🥩死锁

我们前面例子中写的都是只有一把锁的情况,在实际使用中有可能会存在多把锁,此时就可能造成死锁。

  • 死锁:一组执行流中的各个执行流均占有不会释放的锁资源,但因互相申请被其他进程所站用不会释放的锁资源而处于的一种永久等待状态。

通俗来说就是一个线程自己持有锁,并且不会释放,但是还要申请其他线程的锁,此时就容易造成死锁。

  • 一把锁也是会死锁的,连续申请俩次就是死锁。

在上面演示一个线程暂时申请锁失败而阻塞时,就是死锁。

死锁的逻辑链条:

图

可以看到,往往解决一个问题就会引出新的问题,然后再区解决新的问题。

🍚死锁的必要条件

死锁的四个必要条件:

  1. 互斥

这一点不用说,只要用到锁就会互斥。

  1. 请求与保持

请求就是指一个执行流申请其他锁,保持是指不释放自己已经持有的锁。

  1. 不剥夺

一个执行流已经持有锁,在不主动释放前不能强行剥夺。

  1. 环路等待

图

线程A,B,C都持有一把锁,并且不释放。

  • 线程A 申请 线程B持有的锁B
  • 线程B 申请 线程C持有的锁C
  • 线程C 申请 线程A持有的锁A

此时就构成了环路阻塞等待。

只有符合上面四个条件就会造成死锁。而要破坏死锁只要破坏其中一个条件即可。

避免死锁:

四个必要条件中的第一个无法破坏,因为我们使用的就是锁,锁就具有互斥的性质。只能破坏其他三个条件。

  • 避免锁位释放的场景

这是为了破坏请求与保持条件。当一个执行流在申请另一个锁的时候,要先释放已经持有的锁再申请。

  • 加锁顺序一致

这是为了避免形参环路等待,只要不构成环路即可。

  • 资源一次性分配

临界资源尽量一次性分配好,不要分布在太多的地方加锁,这样的话导致死锁的概率就会增加。

避免死锁的算法:

  • 死锁检测算法
  • 银行家算法

有想去的小伙伴可以自己去了解一下这两个算法。

采用算法来避免死锁时,就会有一个执行流专门用来监测其他执行流的状态,一旦发现某个执行流长时间没有执行,就释放它所持有的锁。

图
从解锁的伪代码只能可以看出,解锁是可以由其他线程来完成的,只需要将锁重新赋值到锁的共享资源变量中即可。

🥩总结

互斥锁在实际中能不用就不用,实在没有办法的时候也要尽量少用。

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

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

相关文章

网络编程五--自定义应用层协议

写在前面 前面回声服务器/客户端介绍了如何通过对收发IO的控制实现回声服务器/客户端。 在服务器端应用层的处理&#xff08;协议&#xff09;可以看作是“回声操作”&#xff0c;即回发客户端发来的消息。而在客户端应用层的处理&#xff08;协议&#xff09;则只是简单显示…

Mysql获取指定时间范围数据

MySQL获取某个时间范围内的数据 TO_DAYS(date)函数。 to_days()&#xff1a;返回从0000年至当前日期的总天数。 目录 1、今天(TO_DAYS()) 2、今天昨天(TO_DAYS()) 3.近7天(DATE_SUB()) 5.本月(DATE_FORMAT()) 6.上一月(PERIOD_DIFF()) 7.本季度 8.上季度 9.本年 ​1…

MyBatis介绍、创建与使用

文章目录 一、MyBatis是什么二、学习 MyBatis 的意义三、配置 MyBatis 开发环境&#xff08;一&#xff09;配置 MyBatis 的相关依赖&#xff08;二&#xff09;配置数据库连接字符串和 MyBatis&#xff08;保存的 XML 目录&#xff09;1. 创建并编辑配置文件2. 配置 MyBatis 的…

[GFCTF 2021]ez_calc day3

目录 此时我脑袋产生了几个问题&#xff1a; 但是尝试了几个弱密码发现不对&#xff0c;找一下有没有代码泄露的点。 咦发现ctrlu查看的源码和f12显示的竟然不一样我丢&#xff0c;涨知识了。 js大小写有漏洞之前遇见过 <!--if(req.body.username.toLowerCase() ! admin…

Maven与spring学习

目录 该如何学习Maven&#xff0c;是先该学习spring还是先学习Maven 能讲一下该如何学习Maven吗&#xff1f; 火狐浏览器有能让网页翻译成为中文的插件吗 秋田和柴犬是同一个狗吗 该如何学习Maven&#xff0c;是先该学习spring还是先学习Maven 学习Maven可以与学习Spring同…

FPGA学习总结7:选择译码器实现

本博客以modelsim平台为例&#xff0c;实现了一个三八译码器&#xff1b; Step1.在modelsim创建新的工程&#xff1b; file-》new-》project&#xff0c;给工程命名&#xff0c;添加相应的文件&#xff1b; Step2.添加事先准备的源文件和Testbench文件&#xff1b; 3-8译码器…

p72 内网安全-域横向 CSMSF 联动及应急响应初识

数据来源 演示案例 MSF&CobaltStrike 联动 ShellWEB 攻击应急响应朔源-后门,日志WIN 系统攻击应急响应朔源-后门,日志,流量临时给大家看看学的好的怎么干对应 CTF 比赛 案例1 - MSF&CobaltStrike联动Shell CS下载与安装&#xff1a;cobaltstrike的安装与基础使用_co…

数字信号处理4

昨天是星期天&#xff0c;休息了一天&#xff0c;今天继续学习&#xff1a; 1、连续幅度信号的量化&#xff1a; 一个数字信号是一个数字序列&#xff0c;也就是说这个数字信号就可以用有限个数字来表示。 量化&#xff1a;通过把每个样本值表示为一个有限的数字&#xff0c…

CRM系统建设中需关注哪些关键节点?

随着数字化时代的到来&#xff0c;企业越来越依赖于互联网技术和数据管理&#xff0c;而客户关系管理&#xff08;CRM&#xff09;系统已经成为企业实现数字化转型和提升客户体验的关键工具之一。然而&#xff0c;在构建CRM系统的过程中&#xff0c;一些企业常常被一些关键节点…

键树(Keyword Tree)操作(插入删除查询)-双链树C语言实现_20230508

键树(Keyword Tree)操作&#xff08;插入/删除/查询)-双链树C语言实现_20230508 前言 键树称为数字查找树或者搜索提示树&#xff0c;树的度数d≥2&#xff0c;树中每个结点储存的不是完整的关键字&#xff0c;而是只含有组成关键字的符号&#xff0c;常见情况为字符或数字。…

wsl2 ubuntu ip 自动同步到windows端的vscode remote ssh(wsl2 ubuntu 静态ip 固定ip)

环境信息 ​ wsl2 中linux版本&#xff1a;ubuntu ​ windows版本&#xff1a; win10/win11 问题描述 最近装了wsl2&#xff0c;使用vscode远程连接wsl2时遇到了如下问题&#xff1a; 1、wsl2的ip无法固定 2、wsl2的ssh服务不能自动开启。 尝试了网上许多方法&#xff0c;…

Vue核心 Vue生命周期

1.18. Vue生命周期 1.18.1. 引出生命周期 生命周期 又名生命周期回调函数、生命周期函数、生命周期钩子是什么: Vue在关键时刻帮我们调用的一些特殊名称的函数。生命周期函数的名字不可更改&#xff0c;但函数的具体内容是程序员根据需求编写的。生命周期函数中的this指向是…

JUC并发编程与源码分析笔记13-AbstractQueuedSynchronizer之AQS

前置知识 公平锁和非公平锁可重入锁自旋思想LockSupport数据结构之双向链表设计模式之模板设计模式 AQS入门级别理论知识 是什么 AbstractQueuedSynchronizer&#xff1a;抽象的队列同步器。 用来实现锁或其他同步器组件的公共基础部分的抽象实现&#xff0c;是重量级基础框…

Hive知识回顾2

一、分桶表 1.1分桶表的概念 分桶表也叫做桶表&#xff0c;源自建表语法中bucket单词。是一种用于优化查询而设计的表类型。该功能可以让数据分解为若干个部分易于管理。 在分桶时&#xff0c;我们要指定根据哪个字段将数据分为几桶&#xff08;几个部分&#xff09;。默认规则…

人脸识别中的深度学习

深度学习在人脸识别中的应用 人脸识别的过程包括&#xff1a; 人脸检测人脸对齐特征提取&#xff08;在数学上&#xff0c;实质上是&#xff1a;空间变换&#xff09;特征度量 其中&#xff0c;特征提取与度量&#xff0c;是人脸识别问题中的关键问题&#xff0c;也是相关研究…

使用 ChatGPT 辅助学习——为自己找一个老师

我们每个人都有许多标签&#xff0c;例如高中生、成绩中等、文科&#xff0c;根据这些标签我和其他拥有相同标签的人分配了相同的教程、班级和老师&#xff0c;这可以带来效率上的提升&#xff0c;因为同一份教程、老师就可以服务几十上百人&#xff0c;而无须为每个人定制&…

软件测试工程师的核心竞争力究竟是什么?

对于测试员而言&#xff0c;了解自己岗位的核心竞争力是非常重要的。在职业初期&#xff0c;许多人认为掌握代码才是软件测试的核心竞争力&#xff0c;但是随着经验的增加&#xff0c;我们会发现真正的核心竞争力是由多个方面组成的。 首先&#xff0c;测试人员需要具备良好的测…

BERT 的面试题

BERT 的简介 1、BERT 是什么&#xff1f;它是用来做什么的&#xff1f; BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;是由Google开发的自然语言处理模型&#xff0c;是一种预训练模型&#xff0c;可以用于多种自然语言处理任务&…

【python自动化脚本—实现excel参数化循环调用判定结果】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、需求背景二、我要做什么三、已有接口四、脚本实现五、实现效果图 前言 为提升自己的python能力&#xff0c;记录在工作中实现的自动化脚本&#xff0c;温故…

51单片机(七)定时器

❤️ 专栏简介&#xff1a;本专栏记录了从零学习单片机的过程&#xff0c;其中包括51单片机和STM32单片机两部分&#xff1b;建议先学习51单片机&#xff0c;其是STM32等高级单片机的基础&#xff1b;这样再学习STM32时才能融会贯通。 ☀️ 专栏适用人群 &#xff1a;适用于想要…