Linux中的各种锁及其基本原理

0.概述

通过本文将了解到如下内容:

  • Linux系统的并行性特征
  • 互斥和同步机制
  • Linux中常用锁的基本特性
  • 互斥锁和条件变量

1.Linux的并行性特征

Linux作为典型的多用户、多任务、抢占式内核调度的操作系统,为了提高并行处理能力,无论在内核层面还是在用户层面都需要特殊的机制来确保任务的正确性和系统的稳定运行,就如同一个国家需要各种法律条款来约束每个公民的行为,才能有条不紊地运转。

在内核层面涉及到各种软硬件中断、进线程睡眠、抢占式内核调度、多处理器SMP架构等,因此内核在完成自己工作的时候一直在处理这些资源抢占的冲突问题。

在用户层面的进程,虽然Linux作为虚地址模式操作系统,为每个进程开辟了独立的虚拟地址空间,伪独占式拥有资源,但是仍然存在很多场景不得不产生多个进程共享资源的问题,来完成进程间的通信,但是在Go语言中进程间的通信使用消息来完成,处理地更优雅一些。

在线程层面,线程作为进程的一部分,进程内的多个线程只拥有自己的独立堆栈等少量结构,大部分的资源还是过线程共享,因此多线程的资源占用冲突比进程更加明显,所以多线程编程的线程安全问题是个重难点。综上可知,无论在kernel还是user space都必须有一些机制来确保对于资源共享问题的解决,然后这个机制就是接下来要说的:同步和互斥。

2.同步和互斥机制

  • 基本概念

同步和互斥的概念有时候很容易混淆,可以简单地认为同步是更加宏观角度的一种说法,互斥是冲突解决的细节方法。所谓同步就是调度者让任务按照约定的合理的顺序进行,但是当任务之间出现资源竞争,也就是竞态冲突时,使用互斥的规则强制约束允许数量的任务占用资源,从而解决各个竞争状态,实现任务的合理运行。

同步和互斥密不可分,有资料说互斥是一种特殊的同步,对此我不太理解,不过实际中想明白细节就行,文字游戏没有意义。

简单来说:

  • 同步与互斥机制是用于控制多个任务对某些特定资源的访问策略
  • 同步是控制多个任务按照一定的规则或顺序访问某些共享资源
  • 互斥是控制某些共享资源在任意时刻只能允许规定数量的任务访问
  • 角色分类

整个协调流程涉及的角色本质上只有三类:

  • 不可独占的共享资源
  • 多个使用者
  • 调度者

调度者需要为多个运行任务制定访问使用规则来实现稳定运行,这个调度者可以是内核、可以是应用程序,具体场景具体分析。

  • 重要术语

要很好地理解同步和互斥,就必须得搞清楚几个重要术语:

  • 竞争冒险(race hazard)或竞态条件(race condition)

最早听说这个术语是在模电数电的课程上,门电路出现竞态条件造成错误的结果,在计算机里面就是多个使用者同时操作共享的变量造成结果的不确定。

  • 临界区

临界区域critical section是指多使用者可能同时共同操作的那部分代码,比如自加自减操作,多个线程处理时就需要对自加自减进行保护,这段代码就是临界区域。

3.Linux中常用的锁

在说锁之前还需要知道几个东西:信号量和条件变量。这两个东西和锁有一定的联系和区别,在不同的场合单独使用或者配合实现来说实现安全的并发,至于网上很多说互斥锁是一种信号量的特例,对于这种特例理解不了也罢。信号量和互斥锁的场景不一样,信号量主要是资源数量的管理(池化),实际用的频率远不如互斥锁,文字游戏着实无趣,实用主义至上,掌握高频工具的特点正确使用即可,大可不必过于学术派。在使用锁时需要明确几个问题:

  • 锁的所有权问题 谁加锁 谁解锁 解铃还须系铃人
  • 锁的作用就是对临界区资源的读写操作的安全限制
  • 锁是否可以被多个使用者占用(互不影响的使用者对资源的占用)
  • 占用资源的加锁者的释放问题 (锁持有的超时问题)
  • 等待资源的待加锁者的等待问题(如何通知到其他等着资源的使用者)
  • 多个临界区资源锁的循环问题(死锁场景)

带着问题明确想要达到的目的,我们同样可以根据自己的需求设计锁,Linux现有的锁如果从上面几个问题的角度去理解,就非常容易了。

  • 自旋锁spinlock

概述:

自旋锁是SMP架构中的一种low-level的同步机制。
当线程A想要获取一把自旋锁而该锁又被其它线程锁持有时,线程A会在一个循环中自旋以检测锁是不是已经可用了。对于自旋锁需要注意:

  • 由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在那里自旋,这就会浪费CPU时间。
  • 持有自旋锁的线程在sleep之前应该释放自旋锁以便其它线程可以获得自旋锁。

使用任何锁需要消耗系统资源(内存资源和CPU时间),这种资源消耗可以分为两类:

  • 建立锁所需要的资源
  • 线程被阻塞时锁所需要的资源

spin lock 锁相关的API:

1 int pthread_spin_destroy(pthread_spinlock_t *);
2 int pthread_spin_init(pthread_spinlock_t *, int);
3 int pthread_spin_lock(pthread_spinlock_t *);
4 int pthread_spin_trylock(pthread_spinlock_t *);
5 int pthread_spin_unlock(pthread_spinlock_t *);

1)初始化自旋锁

pthread_spin_init() 用来申请使用自旋锁所需要的资源并且将它初始化为非锁定状态。pshared的取值及其含义:

  • PTHREAD_PROCESS_SHARED:该自旋锁可以在多个进程中的线程之间共享。
  • PTHREAD_PROCESS_PRIVATE: 仅初始化本自旋锁的线程所在的进程内的线程才能够使用该自旋锁。

2)获得一个自旋锁

pthread_spin_lock用来获取(锁定)指定的自旋锁. 如果该自旋锁当前没有被其它线程所持有,则调用该函数的线程获得该自旋锁.否则该函数在获得自旋锁之前不会返回。如果调用该函数的线程在调用该函数时已经持有了该自旋锁,则结果是不确定的。

3)尝试获取一个自旋锁

pthread_spin_trylock会尝试获取指定的自旋锁,如果无法获取则理解返回失败。

4)释放(解锁)一个自旋锁

pthread_spin_unlock用于释放指定的自旋锁。

5)销毁一个自旋锁

pthread_spin_destroy用来销毁指定的自旋锁并释放所有相关联的资源(所谓的所有指的是由pthread_spin_init自动申请的资源)在调用该函数之后如果没有调用pthread_spin_init重新初始化自旋锁,则任何尝试使用该锁的调用的结果都是未定义的。如果调用该

函数时自旋锁正在被使用或者自旋锁未被初始化则结果是未定义的。

//Pthreads提供的Mutex锁操作相关的API主要有:
pthread_mutex_lock (pthread_mutex_t *mutex);
pthread_mutex_trylock (pthread_mutex_t *mutex);
pthread_mutex_unlock (pthread_mutex_t *mutex);
//Pthreads提供的与Spin Lock锁操作相关的API主要有:
pthread_spin_lock (pthread_spinlock_t *lock);
pthread_spin_trylock (pthread_spinlock_t *lock);
pthread_spin_unlock (pthread_spinlock_t *lock);

从实现原理上来讲,Mutex属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线

程A就会被阻塞(blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用

pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止

如果大家去查阅Linux glibc中对pthreads API的实现NPTL(Native POSIX Thread Library) 的源码的话(使用”getconf GNU_LIBPTHREAD_VERSION”命令可以得到我们系统中NPTL的版本号),就会发现pthread_mutex_lock()操作如果没有锁成功的话就会

调用system_wait()的系统调用并将当前线程加入该mutex的等待队列里。而spin lock则可以理解为在一个while(1)循环中用内嵌的汇编代码实现的锁操作(印象中看过一篇论文介绍说在linux内核中spin lock操作只需要两条CPU指令,解锁操作只用一条指令就可以完

成)。有兴趣的朋友可以参考另一个名为sanos的微内核中pthreds API的实现:mutex.c spinlock.c,尽管与NPTL中的代码实现不尽相同,但是因为它的实现非常简单易懂,对我们理解spin lock和mutex的特性还是很有帮助的。

​ 对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间。

​ 对于互斥锁来说,与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗

CPU资源。

​ 因此自旋锁和互斥锁适用于不同的场景。自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景

6)自旋锁例子

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
using namespace std;pthread_spinlock_t lock;void *tfn(void *arg)
{pthread_spin_lock(&lock);printf("thread count %d\n", 3);sleep(1000);pthread_spin_unlock(&lock);}int main(void)
{int initRet =  pthread_spin_init(&lock, NULL);if(initRet != 0){printf("pthread_spin_init error \n");}pthread_t tid;pthread_create(&tid, NULL, tfn, NULL);pthread_create(&tid, NULL, tfn, NULL);pthread_create(&tid, NULL, tfn, NULL);pthread_create(&tid, NULL, tfn, NULL);pthread_create(&tid, NULL, tfn, NULL);pthread_create(&tid, NULL, tfn, NULL);pthread_create(&tid, NULL, tfn, NULL);pthread_create(&tid, NULL, tfn, NULL);pthread_create(&tid, NULL, tfn, NULL);sleep(10000);return 0;
}

结果如下

可以看见lock_demo这个进程占用了大量的cpu资源(虚拟机配置是双核双线程)

top - 16:00:19 up  4:40,  3 users,  load average: 3.00, 3.13, 3.77
Tasks: 467 total,   3 running, 461 sleeping,   0 stopped,   3 zombie
%Cpu0  :100.0 us,  0.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :  4.3 us,  1.7 sy,  0.0 ni, 94.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  : 98.7 us,  1.3 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  : 99.7 us,  0.3 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem:   4033556 total,  2313104 used,  1720452 free,   268612 buffers
KiB Swap:  4191228 total,        0 used,  4191228 free.   972264 cached MemPID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND    7793 qgm       20   0   88940   1248   1064 S 299.5  0.0 362:27.11 Lock_demo  1343 root      20   0  342296  72816  34304 S   3.0  1.8   2:16.52 Xorg       7494 qgm       20   0  661452  22188  13476 S   1.3  0.6   0:05.03 gnome-ter+ 2410 qgm       20   0 1517276  90216  40208 R   1.0  2.2   2:39.74 compiz     1656 root      20   0  158744   7396   4948 S   0.3  0.2   0:32.01 vmtoolsd   1 root      20   0   33768   3064   1460 S   0.0  0.1   0:02.62 init       2 root      20   0       0      0      0 S   0.0  0.0   0:00.10 kthreadd   3 root      20   0       0      0      0 S   0.0  0.0   0:00.11 ksoftirqd+ 4 root      20   0       0      0      0 S   0.0  0.0   0:00.00 kworker/0+ 5 root       0 -20       0      0      0 S   0.0  0.0   0:00.00 kworker/0+
  • 互斥锁mutex

使用者使用互斥锁时在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作,谁加锁谁释放,其他使用者没有释放权限。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。 区别于自旋锁,互斥锁无法获取锁时将阻塞睡眠,需要系统来唤醒,可以看出来自旋转自己原地旋转来确定锁被释放了,互斥锁由系统来唤醒,但是现实并不是那么美好的,因为很多业务逻辑系统是不知道的,仍然需要业务线程执行while来轮询是否可以重新加锁。考虑这种情况:解锁时有多个线程阻塞,那么所有该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待,对其他线程而言就是虚假唤醒。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源。

​ 例子

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
pthread_mutex_t lock;int count = 0;void *tfn(void *arg)
{//pthread_mutex_lock(&lock);for(int i = 0;i < 100000;i++){count++;}//pthread_mutex_unlock(&lock);printf("calc count result %d\n", count);}int main(void)
{int initRet =  pthread_mutex_init(&lock, NULL);if(initRet != 0){printf("pthread_mutex_init error \n");return -1;}pthread_t tid;pthread_create(&tid, NULL, tfn, NULL);pthread_create(&tid, NULL, tfn, NULL);pthread_create(&tid, NULL, tfn, NULL);sleep(10000);return 0;
}

运行结果

//加互斥锁结果
calc count result 10000
calc count result 10000
calc count result 10000
//不加互斥锁结果
calc count result 9024
calc count result 17865
calc count result 24205
  • 读写锁rwlock

读写锁也叫共享互斥锁:读模式共享和写模式互斥,本质上这种非常合理,因为在数据没有被写的前提下,多个使用者读取时完全不需要加锁的。读写锁有读加锁状态、写加锁状态和不加锁状态三种状态,当读写锁在写加锁模式下,任何试图对这个锁进行加锁的线程都会被阻塞,直到写进程对其解锁。

*读优先的读写锁*:读写锁rwlock默认的也是读优先,也就是:当读写锁在读加锁模式先,任何线程都可以对其进行读加锁操作,但是所有试图进行写加锁操作的线程都会被阻塞,直到所有的读线程都解锁,因此读写锁很适合读次数远远大于写的情况。这种情况需要考虑写饥饿问题,也就是大量的读一直轮不到写,因此需要设置公平的读写策略。在一次面试中曾经问到实现一个写优先级的读写锁,感兴趣的可以想想如何实现。

  • RCU锁

RCU锁是读写锁的扩展版本,简单来说就是支持多读多写同时加锁,多读没什么好说的,但是对于多写同时加锁,还是存在一些技术挑战的。RCU锁翻译为Read Copy Update Lock,读-拷贝-更新 锁。Copy拷贝:写者在访问临界区时,写者将先拷贝一个临界区副本,然后对副本进行修改;Update更新:RCU机制将在在适当时机使用一个回调函数把指向原来临界区的指针重新指向新的被修改的临界区,锁机制中的垃圾收集器负责回调函数的调用。更新时机:没有CPU再去操作这段被RCU保护的临界区后,这段临界区即可回收了,此时回调函数即被调用。从实现逻辑来看,RCU锁在多个写者之间的同步开销还是比较大的,涉及到多份数据拷贝,回调函数等,因此这种锁机制的使用范围比较窄,适用于读多写少的情况,如网络路由表的查询更新、设备状态表更新等,在业务开发中使用不是很多。

  • 可重入锁和不可重入锁
  • 递归锁recursive mutex 可重入锁(reentrant mutex)
  • 非递归锁non-recursive mutex 不可重入锁(non-reentrant mutex)

Windows下的Mutex和Critical Section是可递归的。Linux下的pthread_mutex_t锁默认是非递归的。在Linux中可以显式设置PTHREAD_MUTEX_RECURSIVE属性,将pthread_mutex_t设为递归锁避免这种场景。 同一个线程可以多次获取同一个递归锁,不会产生死锁。而如果一个线程多次获取同一个非递归锁,则会产生死锁。

如下代码对于非递归锁的死锁示例:

MutexLock mutex;
void testa()
{  mutex.lock();  do_sth();mutex.unlock();
}
void testb()
{  mutex.lock();   testa();  mutex.unlock();
}

代码中testb使用了mutex并且调用testa,但是testa中也调用了相同的mutext,这种场景下如果mutex是非递归的就会出现死锁。

可递归锁

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
using namespace std;#define RECURSIVE ""pthread_mutex_t lock;
pthread_mutexattr_t attr;int count = 0;void callA()
{pthread_mutex_lock(&lock);printf("ccall A \n");pthread_mutex_unlock(&lock);}void callB()
{pthread_mutex_lock(&lock);printf("call B \n");callA();pthread_mutex_unlock(&lock);}void *tfn(void *arg)
{callB();
}int main(void)
{int ret;
#ifdef RECURSIVEif((ret = pthread_mutexattr_init(&attr)) != 0){printf("create mutex attr failed \n");exit(1);}pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE);pthread_mutex_init(&lock,&attr);
#elsepthread_mutex_init(&lock,NULL);
#endifpthread_t tid;pthread_create(&tid, NULL, tfn, NULL);sleep(10000);return 0;
}
  • 条件变量condition variables

条件变量是用来等待线程而不是上锁的,通常和互斥锁一起使用。互斥锁的一个明显的特点就是某些业务场景中无法借助系统来唤醒,仍然需要业务代码使用while来判断,这样效率本质上比较低。而条件变量通过允许线程阻塞和等待另一个线程发送信号来弥补互斥锁的不足,所以互斥锁和条件变量通常一起使用,来让条件变量异步唤醒阻塞的线程。

条件变量和互斥锁的典型使用就是生产者和消费者模型,这个模型非常经典,也在面试中经常被问到,示例代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
using namespace std;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t notfull = PTHREAD_COND_INITIALIZER;  //是否队满
pthread_cond_t notempty = PTHREAD_COND_INITIALIZER; //是否队空int currentCount = 0;void* produce(void* arg)
{while(true){pthread_mutex_lock(&mutex);currentCount++;printf("create a product \n");if(currentCount == 10){printf("full! producer is waiting\n");//等待队不满pthread_cond_wait(&notfull, &mutex);}//发出队非空的消息pthread_cond_signal(&notempty);pthread_mutex_unlock(&mutex);sleep(1);}return (void*)1;
}void* consume(void* arg)
{while(true){pthread_mutex_lock(&mutex);if(currentCount == 0){printf("empty! consumer is waiting\n");//等待队不空pthread_cond_wait(&notempty, &mutex);}currentCount--;printf("use a product \n");//发出队非空的消息pthread_cond_signal(&notfull);pthread_mutex_unlock(&mutex);sleep(1);}return (void*)2;
}
int main()
{pthread_t thid1;pthread_t thid2;int ret1;int ret2;pthread_create(&thid1, NULL, produce, NULL);pthread_create(&thid2, NULL, consume, NULL);pthread_join(thid1, (void**)&ret1);pthread_join(thid2, (void**)&ret2);return 0;
}

其中pthread_cond_wait的使用是个需要注意的地方:pthread_cond_wait()是先将互斥锁解开,并陷入阻塞,直到pthread_signal()发出信号后pthread_cond_wait()再加上锁,然后退出。

Linux中的各种锁及其基本原理相关推荐

  1. Linux中解除带锁的文件夹

    处理方法: ①.打开终端,进入该目录下 $ cd /usr/local/spark/mycode/remdup ②.输入命令 $ sudo chmod 777 target

  2. 解除linux中文件被锁状态,linux – 为什么即使文件被锁定,File :: FcntlLock的l_type总是“F_UNLCK”?...

    下面的Perl子例程使用File :: FcntlLock来检查文件是否被锁定. 为什么它返回0并且打印/tmp/test.pid被解锁.即使文件被锁定了? sub getPidOwningLock ...

  3. linux之mutex(互斥锁)

    在Posix Thread中定义有一套专门用于线程同步的mutex函数 1. 创建和销毁 有两种方法创建互斥锁,静态方式和动态方式.POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZ ...

  4. Linux 2.6内核中新的锁机制--RCU [转]

    2005 年 7 月 01 日 本文详细地介绍了 Linux 2.6 内核中新的锁机制 RCU(Read-Copy Update) 的实现机制,使用要求与典型应用. 一. 引言 众所周知,为了保护共享 ...

  5. linux内核中锁有哪些,Linux内核中有哪些锁

    Linux内核中的各种锁 在LInux操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实象多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问.尤其是在多处理器系统上,更需 ...

  6. java中锁的基本原理和升级:偏向锁、轻量级锁、重量级锁

    目录 由一个问题引发的思考 多线程对于共享变量访问带来的安全性问题 线程安全性 思考如何保证线程并行的数据安全性 synchronized 的基本认识 synchronized 的基本语法 synch ...

  7. Linux 2.6内核中新的锁机制--RCU

    转载自: Linux 2.6内核中新的锁机制--RCU 一. 引言 众所周知,为了保护共享数据,需要一些同步机制,如自旋锁(spinlock),读写锁(rwlock),它们使用起来非常简单,而且是一种 ...

  8. 探秘最新Linux内核中的自旋锁

    一.前言 目前最新内核中的自旋锁已经进化成queued spinlock,因此需要一篇新的自旋锁文档来跟上时代.此外,本文将不再描述基本的API和应用场景,主要的篇幅将集中在具体的自旋锁实现上.顺便说 ...

  9. Linux中锁的总结

    目录 1 前言 2 注意事项 2.1 明确锁的范围 2.2 减少锁的粒度 3 避免死锁的建议 1 前言 实际开发过程中,使用锁会带来一定性能的损失,但如果使用锁也能满足性能要求,对于锁的使用就无妨.使 ...

最新文章

  1. LeetCode: 108. Convert Sorted Array to Binary Search Tree
  2. Python之gmplot:gmplot库的简介、安装、使用方法之详细攻略
  3. B-树 B+树复习总结
  4. ICEM(1)—边界结构网格绘制
  5. Cacti迁移RRA数据迁移脚本
  6. python实现合并两个文件并打印输出
  7. Linux应用基本命令实验,实验二 linux基本命令的使用
  8. SetMutableGraph
  9. C语言的变量作用域及头文件
  10. css中换行的几种方式
  11. 离散LQR与iLQR的推导思路
  12. 主机访问虚拟机Web服务器
  13. Netty之线程唤醒wakeup
  14. ftp 连接失败。500 OOPS: cannot change directory:
  15. 电能计量芯片HLW8110/HLW8112
  16. C# 通用方法MD5计算
  17. 股票K线5,15,30,60分钟数据接口
  18. [PATCH] ARM: add dtbImage.dt and dtbuImage.dt rules
  19. 【论文分享】★★★「SOTA」小样本图神经网络分类模型 HGNN:Hybrid Graph Neural Networks for Few-Shot Learning
  20. Mysql数据库的安全策略

热门文章

  1. PELCO-D协议校验位
  2. 【2014-06-16】AntiSpy 2.2 (新增进程树模式,结束进程树等功能)
  3. android 按钮带图标 阴影_Android实现图片添加阴影效果的2种方法
  4. 淘宝推出了定制版本的 JVM
  5. 2021年西式面点师(初级)模拟试题及西式面点师(初级)证考试
  6. iOS应用签名管理工具
  7. lucene查询解析器语法
  8. JetsonNano人脸识别(一)安装配置
  9. 批量修改AD账号的UPN后缀(同适用于解决外网自动配置Exchange邮箱)
  10. 一文读懂现代化的智能天线技术