• linux驱动移植进程同步之自旋锁


    一、自旋锁(spinlock)

    1.1  什么是自旋锁

    自旋锁(spinlock)是一种典型的对临界资源进行互斥访问的手段,其名称来源于它的工作方式。自旋锁和信号量的主要区别在于,如果进程没有获取到自旋锁,就一直循环在那里看是否该自旋锁的保持者已经释放了锁。

    自旋锁的实现有多种:比如CAS和ticket spinlock;目前linux自选锁的实现采用了ticket spinlock,我们会在后面源码实现时介绍。

    • CAS:spinlock用一个整形变量表示,其初始值为1,表示available的状态。当一个CPU获得spinlock后,会将该变量的值设为0,之后其他CPU试图获取这个spinlock时,会一直等待,直到CPU A释放spinlock,并将该变量的值设为1。其比较和交换值是通过cas汇编指令实现;
    • ticket spinlock:这类似于你去银行柜台办理业务,假设当前银行只有一个柜台,你需要在自助机上获得一个排队号码(相当于一个ticket),然后当柜台叫到的号码与你手中的号码一致时,你将坐上柜台前面的椅子,此时柜台为你服务,这也是这种实现方式被称为"ticket spinlock"的原因;

    1.2 自旋锁具有的特点

    自旋锁可以用于多核系统(SMP),也可以用于单核系统(UP),自旋锁在实现的时候调用preempt_disable关闭了内核抢占。也就是说运行在一个CPU的代码使用spin_lock加锁之后,基于该CPU的内核抢占就被禁止了。因此会产生以下影响:

    • 在单核系统:只需要禁止内核抢占,等同于关闭了进程切换,从而就不存在进程同步的问题。由于禁止了内核抢占,如果进程获取自旋锁之后,在临界区中睡眠,将会导致其他进程都无法获取CPU而运行,从而不能唤醒睡眠的自旋锁,因此禁止在自旋锁中使用睡眠等函数(除了中断,但是中断通常不会唤醒睡眠的自旋锁);
    • 在多核系统:虽然禁止了当前CPU内核抢占,但是如果存在多个CPU,仍然存在多个CPU对自旋锁共享变量同时访问的问题,因此在多核系统除了关闭CPU内核抢占、还需要通过独占指令ldrex、strex实现共享变量的互斥访问;

    自旋锁的特点有:

    • spinlock是一种死等的锁机制;
    • semaphore可以允许多个执行单元进入,spinlock不行,一次只能有一个执行单元获取锁并进入临界区,其他的执行单元都是在门口不停的死等;
    • 执行时间短,由于spinlock死等这种特性,如果临界区执行时间太长,那么不断的在临界区门口“死等”的那些执行单元会浪费CPU;
    • 可以在中断上下文中执行,由于不休眠,因此spinlock可以在中断上下文中适用;

    1.3 自旋锁禁止内核抢占

    自旋锁禁止内核抢占这是为什么呢

    在一个打开CONFIG_PREEMT特性的Linux系统中,一个在内核态执行的进程也有可能被切换出处理器,典型的比如当前进程正在内核态执行某一系统调用时,发生了一个外部中断,当中断处理函数返回时,由于内核的可抢占性,此时将会出现一个调度点,如果CPU的就绪队列中出现一个比当前被中断进程优先级更高的进程,那么被中断的进程将会被换出处理器,即便此时他正运行在内核态,单核处理器上的这种因为内核可抢占性所导致的两个不同进程并发执行的情形,非常类似SMP系统上运行在不同处理器上的进程之间的并发,因此为了保护共享的资源不会受到破坏,必须在进入临界区之前关闭内核的可抢占性。

    1.4 自旋锁的使用

    定义自旋锁:

    spinlock_t lock;

    初始化自旋锁:

    spin_lock_init(&lock);

    获得自旋锁:

    spin_lock(&lock);

    该宏用于获得自旋锁lock,如果能够立即获得锁,它就马上返回,否则,它将自旋在那里,直到该自旋锁的保持者释放;

    spin_trylock(&lock)

    该宏尝试获得自旋锁lock,如果能立即获得锁,它获得锁并返回非0值,否则返回0,实际上不再"在原地打转";

    释放自旋锁:

    spin_unlock(&lock);

    该函数释放自旋锁lock, 它与spin_trylock或spin_lock配对使用。

    1.5 中断情况下自旋锁的使用

    尽管用了自旋锁可以保证临界区不受别的CPU和本CPU内的内核抢占打扰,但是得到锁的代码路径在执行临界区的时候, 还可能受到中断和底半部的影响。

    这里以硬件中断为例,试想一下,假设一个CPU上的进程A持有了一个spinlock,发生中断后,该CPU转而执行对应的hardirq。如果该hardirq也试图去持有这个spinlock,那么将无法获取成功,导致hardirq无法退出。在hardirq主动退出之前,进程A是无法继续执行以释放spinlock的,最终将导致该CPU上的代码不能继续向前运行,形成死锁(dead lock)

    为了防止这种影响,所以与中断屏蔽联系使用。spin_lock /spin_unlock是自旋锁机制的基础,它们和:

    • 关中断local_irq_disable/开中断local_irq_enable
    • 关底半部local_bh_disable/开底半部local_bh_enable
    • 关中断并保存状态字local_irq_save/开中断并恢复状态字local_irq_restore

    结合就形成了整套自旋锁机制,关系如下:

    spin_lock_irq() = spin_lock() + local_irq_disable()
    spin_unlock_irq() = spin_unlock() + local_irq_enable()
    spin_lock_irqsave() = spin_lock() + local_irq_save()
    spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
    spin_lock_bh() = spin_lock() + local_bh_disable()
    spin_unlock_bh() = spin_unlock() + local_bh_enable()

    在多核编程的时候, 如果进程和中断可能访问同一片临界资源,我们一般需要在进程上下文中调用spin_lock_irqsave /spin_unlock_irqrestore,在中断上下文中调用spin_lock/spin_unlock。

    例如,在CPU0上,无论是进程上下文,还是中断上下文获得了自旋锁,此后,如果CPU1无论是进程上下文, 还是中断上下文, 想获得同一自旋锁,都必须忙等待,这避免一切核间并发的可能性。同时,由于每个核的进程上下文持有锁的时候用的是spin_lock_irqsave,所以该核上的中断是不可能进入的,这避免了核内并发的可能性。

    二、自旋锁的源码实现

    2.1 spinlock_t

    spinlonk_t结构体定义位于include/linux/spinlock_types.h文件中:

    typedef struct spinlock {
            union {
                    struct raw_spinlock rlock;
            };
    } spinlock_t;

    在该文件,定位到struct raw_spinlock结构体:

    typedef struct raw_spinlock {
            arch_spinlock_t raw_lock;
    } raw_spinlock_t;

    最后定位到arch_spinlock_t,该函数也是和硬件体系相关的函数,位于arch/arm/include/asm/spinlock_types.h:

    typedef struct {
            union {
                    u32 slock;
                    struct __raw_tickets {
    #ifdef __ARMEB__                        // 大端 高字节保存在低位
                            u16 next;
                            u16 owner;
    #else
                            u16 owner;
                            u16 next;
    #endif
                    } tickets;
            };
    } arch_spinlock_t;

    owner表示持有这个数字的进程可以获取自旋锁;

    next表示如果后续再有进程请求获取这个自旋锁,就给它分配这个数字;

    2.2 spin_lock_init

    宏spin_lock_init位于include/linux/spinlock.h文件中:

    #define spin_lock_init(_lock)                           \
    do {                                                    \
            spinlock_check(_lock);                          \
            raw_spin_lock_init(&(_lock)->rlock);            \
    } while (0)

    在当前文件定位到宏raw_spin_lock_init:

    # define raw_spin_lock_init(lock)                               \
            do { *(lock) = __RAW_SPIN_LOCK_UNLOCKED(lock); } while (0)
    #endif

    再次定位到宏__RAW_SPIN_LOCK_UNLOCKED,该宏位于include/linux/spinlock_types.h:

    #define __RAW_SPIN_LOCK_INITIALIZER(lockname)   \
            {                                       \
            .raw_lock = __ARCH_SPIN_LOCK_UNLOCKED,  \
            SPIN_DEBUG_INIT(lockname)               \
            SPIN_DEP_MAP_INIT(lockname) }
    
    #define __RAW_SPIN_LOCK_UNLOCKED(lockname)      \
            (raw_spinlock_t) __RAW_SPIN_LOCK_INITIALIZER(lockname)

    这里使用__ARCH_SPIN_LOCK_UNLOCKED初始化结构体成员raw_lock,该宏位于arch/arm/include/asm/spinlock_types.h:

    #define __ARCH_SPIN_LOCK_UNLOCKED       { { 0 } }

    这样owner、next都被初始化为0。

    2.3  spin_lock

    我们再来看一下获取自旋锁宏spin_lock,位于include/linux/spinlock.h:

    static __always_inline void spin_lock(spinlock_t *lock)
    {
            raw_spin_lock(&lock->rlock);
    }

    定位到当前文件宏raw_spin_lock:

    #define raw_spin_lock(lock)     _raw_spin_lock(lock)

    _raw_spin_lock有两个实现:

    • 位于include/linux/spinlock_api_up.h   单核CPU;

    • 位于kernel/locking/spinlock.c  多核CPU;

    2.4 _raw_spin_lock单核实现

    先介绍include/linux/spinlock_api_up.h中的实现:

    #define _raw_spin_lock(lock)                    __LOCK(lock)
    #define ___LOCK(lock) \
      do { __acquire(lock); (void)(lock); } while (0)
    #define __LOCK(lock) \
      do { preempt_disable(); ___LOCK(lock); } while (0)

    这里___LOCK函数啥也没做,所以我们重点关注preempt_disable,这个函数是会禁止内核抢占。如果不禁止内核抢占,那么实际上_raw_spin_lock就是什么也没做,就会出现因内核抢占导致两个不同的进程并发执行的问题。

    2.5 _raw_spin_lock多核实现

    然后再来看kernel/locking/spinlock.c中的实现:

    void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
    {
            __raw_spin_lock(lock);
    }

    __raw_spin_lock定义在include/linux/spinlock_api_smp.h中:

    static inline void __raw_spin_lock(raw_spinlock_t *lock)
    {
            preempt_disable();
            spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
            LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
    }

    首先禁止内核抢占,然后执行spin_acquire,该函数位于include/linux/lockdep.h:

    #define lock_acquire_exclusive(l, s, t, n, i)           lock_acquire(l, s, t, 0, 1, n, i)
    #define spin_acquire(l, s, t, i)                lock_acquire_exclusive(l, s, t, NULL, i)
    # define lock_acquire(l, s, t, r, c, n, i) do { } while (0)

    可以看到这个函数啥也没做,我们最来到LOCK_CONTENDED,也是位于include/linux/lockdep.h:

    #define LOCK_CONTENDED(_lock, try, lock)                        \
    do {                                                            \
            if (!try(_lock)) {                                      \
                    lock_contended(&(_lock)->dep_map, _RET_IP_);    \
                    lock(_lock);                                    \
            }                                                       \
            lock_acquired(&(_lock)->dep_map, _RET_IP_);                     \
    } while (0)

    第三个参数为do_raw_spin_lock,位于kernel/locking/spinlock_debug.c:

    /*
     * We are now relying on the NMI watchdog to detect lockup instead of doing
     * the detection here with an unfair lock which can cause problem of its own.
     */
    void do_raw_spin_lock(raw_spinlock_t *lock)
    {
            debug_spin_lock_before(lock);
            arch_spin_lock(&lock->raw_lock);
            mmiowb_spin_lock();
            debug_spin_lock_after(lock);
    }

    定位到arm体系架构代码,arch/arm/include/asm/spinlock.h:

    /*
     * ARMv6 ticket-based spin-locking.
     *
     * A memory barrier is required after we get a lock, and before we
     * release it, because V6 CPUs are assumed to have weakly ordered
     * memory.
     */
    
    static inline void arch_spin_lock(arch_spinlock_t *lock)
    {
            unsigned long tmp;
            u32 newval;
            arch_spinlock_t lockval;
    
            prefetchw(&lock->slock);
            __asm__ __volatile__(
    "1:     ldrex   %0, [%3]\n"
    "       add     %1, %0, %4\n"
    "       strex   %2, %1, [%3]\n"
    "       teq     %2, #0\n"
    "       bne     1b"
            : "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
            : "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
            : "cc");
    
            while (lockval.tickets.next != lockval.tickets.owner) {
                    wfe();
                    lockval.tickets.owner = READ_ONCE(lock->tickets.owner);
            }
    
            smp_mb();
    }

    这里我们就不具体分析这个汇编代码了,这里汇编代码本质上还是利用CPU的独占访问指令实现对slock值的修改,大致介绍一下:

    • 这里我们仍然以R0表示%0,R1表示%1,R2表示%2,R3表示%3(指向slock的地址);
    • 独占访问指令,取出slock中的值到R0;
    • R1 = R0 + (1 << TICKET_SHIFT),TICKET_SHIFT值为16;
    • 独占访问指令,执行成功,[R3]=R1,R2=0;
    • 测试R2=0?
    • 如果R2=0,执行完成,否则继续循环执行;

    执行成功之后等价于执行如下指令:

    lockval=lock->slock;   // 保存旧值
    lock->slock += 1<<16;  // 修改后的新值 slock是一个union,由next和owner组成,等价于next++;
    newlock = lock->slock;

    然后再来看一下C代码:

    • 判断next是否和ower相等,如果不相等,则进程一直等待;
    • 如果相等,获取到锁;

    举个例子假设有三个进程执行这段代码,lock->slock初始值为0:

     指令 进程1 进程2 进程3 影响

     

     ldrex R0, [R3]

     

    设置独占标记  R0=[R3 ]

     

     add R0, R0, R4

     

     R0=R0+(1<<16)

     ldrex R0, [R3]

     

     

    设置独占标记  R0=[R3 ]

    4

        ldrex R0, [R3]

    设置独占标记  R0=[R3 ]

    5     add R0, R0, R4 R0=R0+(1<<16)

    6

     add R0, R0, R4

     

     

     R0=R0+(1<<16)

    7

     strex R2, R1, [R3]

     

     

    执行成功,R1写回[R3],清除独占标记  

    8

     

     strex R1, R0, [R3]

     

    没有独占标记,执行失败

    9

     teq R2, #0

     

     

     相等 R2=0

    10

        strex R2, R1, [R3]

    没有独占标记,执行失败

    11

     

     teq R2, #0

     

    不相等 R2=1

    12

     

     b 1b

       跳转

    13

        teq R2, #0 不相等 R2=1
    14     b 1b  跳转
    15 汇编执行完毕    

    lockval=R1=0                        当前保存进程值

    lock->slock=R3=1<<16        lock->tickets.next=1

    16 lockval.tickets.next != lockval.tickets.owner    

    条件不满足,获取spinlock,进入临界区

    17   再次经历上面步骤,不过此时lock->slock初始值为1<<16    
    18   ....    
    19     ...  
    20   汇编执行完毕  

    lockval=R1=1<<16             当前保存进程值

    lock->slock=R3=2<<16      lock->tickets.next=2

    21     汇编执行完毕

    lockval=R1=2<<16             当前保存进程值

    lock->slock=R3=3<<16      lock->tickets.next=3

    22   lockval.tickets.next != lockval.tickets.owner  

    lockval=1<<16 ,条件成立 开始死等

    23   lockval.tickets.owner = READ_ONCE(lock->tickets.owner)   lockval=1<<16      
    24     lockval.tickets.next != lockval.tickets.owner lockval=2<<16 ,条件成立  开始死等
    25     lockval.tickets.owner = READ_ONCE(lock->tickets.owner) lockval不变
    26 spin_unlock     lock->slock=3<<16+1     lock->tickets.owner=1
    27   lockval.tickets.next != lockval.tickets.owner   成立 开始死等
    28   lockval.tickets.owner = READ_ONCE(lock->tickets.owner)   lockval=1<<16 +1     
        lockval.tickets.next != lockval.tickets.owner   条件不满足,获取spinlock,进入临界区

    代码大致流程如下,三个进程同时修改lock->slock(联合体,或者说lock->tickets),这个变量是三个进程共享的:

    • 刚开始lock->tickets.owner=0,lock->tickets.next=0;
    • 进程1获得独占锁,成功修改lock->slock,此时当前进程局部变量locakval.tickets.owner=0,locakval.tickets.next=0,lock->tickets.owner=0,lock->tickets.next=1;
    • 进程2获取独占锁,成功修改lock-slock,此时当前进程局部变量locakval.tickets.owner=0,locakval.tickets.next=1,lock->tickets.owner=0,lock->tickets.next=2;
    • 进程2满足条件lockval.tickets.next != lockval.tickets.owner,开始死等;
    • 进程3获取独占锁,成功修改lock-slock,此时当前进程局部变量locakval.tickets.owner=0,locakval.tickets.next=2,lock->tickets.owner=0,lock->tickets.next=3;
    • 进程3满足条件lockval.tickets.next != lockval.tickets.owner,开始死等;
    • 此时第一个进程释放spinlock,则执行lock->tickets.owner++,lock->tickets.owner=2;
    • 虽然此时第二个进程和第三个进程都在等待spinlock,但是因为第二个进程的修改了lockval.tickets.owner,满足了lockval.tickets.next != lockval.tickets.owner,所以第二个进程可以获取到spinlock,第三个进程则继续等待;

    这样保证了spinlock的唤醒机制是先到先唤醒,后到后唤醒,保证了公平性;这种自旋锁的实现方式被称作ticket spinlock,此外还有基于CAS实现的自旋锁,具体可以查看Linux中的spinlock机制[一] - CAS和ticket spinlock

    2.6 自旋锁的效率问题

    spinlock可以让进程到达的先后顺寻获锁,形成有序竞争,在多核系统中,根据cache的一致性协议:

    • 如果spinlock的值没有修改,那么忙等待时,试图获取spinlock锁的CPU,只需要不断的读取包含自己这个spinlock变量的cache line的值就可以了,不需要从spinlock变量所在的内存位置读取。
    • 如果spinlock被修改,那么所有试图获取spinlock的CPU对应的cache line都会被invalidate,从之前代码分析我们了解到,这些忙等待的进程会不断的读取spinlock的值,所以invalidate状态意味着此时,它们必须重新从内存读取新的spinlock的值到自己的cache line中。

    而实际上,其中只会有一个CPU,也就是队列中最先到达的那个CPU,接下来可以获取spinlock,也只有它的cache line被invalidate才是有意义的,对于其他的CPU来说,这就是做无用功。内存比cache慢那么多,开销可不小。

    2.7 总结

    看完这个我们再来通俗解释一下自旋锁的实现:

    • 通过strex和ldrex指令实现对独占资源lock->slock值的互斥访问,该值初始为0;
    • 当第一个进程获取到修改lock->slock权限时,进程使用局部变量locklval保存修改前的值,locklval=0,然后修改lock->slock.next++;
    • 进程lockval值next部分可以理解为分配给这个进程的数字为0,lock->slock.owner部分可以理解为持有数字0的进程可以获得锁;两者相等,因此第一个进程直接获取spinlock锁;
    • 当第二个进程获取到修改lock->slock权限时,进程使用局部变量locklval保存修改前的值,locklval=1<<16,然后修改lock->slock.next++;
    • 进程lockval值next部分可以理解为分配给这个进程的数字为1,lock->slock.owner部分可以理解为持有数字0的进程可以获得锁;两者不相等,因此第二个进程死等;
    • 当第一个进程释放spinlock锁,修改lock->slock.owner++;此时可以理解为持有数字1进程可以获得锁;
    • 此时第二个进程获取spinlick锁;

    三、自旋锁示例程序

    3.1 使用注意事项

    • 自旋锁实际上是忙等锁,当锁不可用时,CPU一直循环执行“测试并设置”该锁直到可用而取得该锁,CPU在等待自旋锁时不做任何有用的工作,仅仅是等待。因此,只有在占用锁的时间极短的情况下,使用自旋锁才是合理的。当临界区很大,或有共享设备的时候,需要较长时间占用锁,使用自旋锁会降低系统的性能;
    • 自旋锁可能导致系统死锁。引发这个问题最常见的情况是递归使用一个自旋锁,即如果一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁,则该CPU将死锁;
    • 在自旋锁锁定期间不能调用可能引起进程调度的函数。如果进程获得自旋锁之后再阻塞,如调用copy_from_user 、 copy_to_user、kmalloc 和msleep等函数,则可能导致内核的崩溃;
    • 在单核情况下编程的时候,也应该认为自己的CPU是多核的,驱动特别强调跨平台的概念。比如,在单CPU的情况下,若中断和进程可能访问同一临界区,进程里调用spin_lock_irqsave是安全的,在中断里其实不调用spin_lock也没有问题, 因为spin_lock_irqsave可以保证这个CPU的中断服务程序不可能执行。但是,若CPU变多核,spin_lock_irqsave不能屏蔽另外一个核的中断,所以另外一个核就可能造成并发问题。因此,无论如何,我们在中断服务程序里也应该调用spin_lock;

    3.2 示例程序

    修改信号量示例里面的驱动程序:

    • 在驱动程序中首先定义并初始化一个自旋锁:
    • 增加驱动程序里的open函数和close函数里对自旋锁的操作:
    #include <linux/module.h>
    #include <linux/cdev.h>
    #include <linux/fs.h>
    
    #define OK   (0)
    #define ERROR  (-1)
    
    /* 自旋锁 */
    static spinlock_t lock;
    static int count = 0;
    
    int hello_open(struct inode *p, struct file *f)
    {
        /* 获取自旋锁 */
        spin_lock(&lock);
        if(count >= 1){
            spin_unlock(&lock);        
            printk("device busy,hello_open failed");
            return ERROR;
        }
        count++;
        spin_unlock(&lock);        
        printk("hello_open\n");
        return 0;
    }
    
    ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l)
    {
        printk("hello_write\n");
        return 0;
    }
    
    ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
    {
        printk("hello_read\n");
        return 0;
    }
    
    int hello_close(struct inode *inode, struct file *file)
    {
         /* 获取自旋锁 */
        spin_lock(&lock);
        count--;
        spin_unlock(&lock);        
        return 0;
    }
    
    struct file_operations hello_fops = {
        .owner   =   THIS_MODULE,
        .open    =   hello_open,
        .read    =   hello_read,
        .write   =   hello_write,
        .release =   hello_close,
    };
    
    dev_t devid;                      // 起始设备编号
    struct cdev hello_cdev;          // 保存操作结构体的字符设备 
    struct class *hello_cls;
    
    int hello_init(void)
    {
        
        /* 动态分配字符设备: (major,0) */
        if(OK == alloc_chrdev_region(&devid, 0, 1,"hello")){   // ls /proc/devices看到的名字
            printk("register_chrdev_region ok\n");
        }else {
            printk("register_chrdev_region error\n");
            return ERROR;
        }
        
         cdev_init(&hello_cdev, &hello_fops);
         cdev_add(&hello_cdev, devid, 1);
    
    
        /* 创建类,它会在sys目录下创建/sys/class/hello这个类  */
         hello_cls = class_create(THIS_MODULE, "hello");
         if(IS_ERR(hello_cls)){
             printk("can't create class\n");
             return ERROR;
         }
        /* 在/sys/class/hello下创建hellos设备,然后mdev通过这个自动创建/dev/hello这个设备节点 */
         device_create(hello_cls, NULL, devid, NULL, "hello"); 
    
         /* 初始化自旋锁 */
         spin_lock_init(&lock);
         return 0;
    }
    
    void __exit hello_exit(void)
    {
        printk("hello driver exit\n");
        /* 注销类、以及类设备 /sys/class/hello会被移除*/
        device_destroy(hello_cls, devid);
        class_destroy(hello_cls);
    
        cdev_del(&hello_cdev);
        unregister_chrdev_region(devid, 1);
        return;
    }
    
    
    module_init(hello_init);
    module_exit(hello_exit);
    MODULE_LICENSE("GPL");

    参考文章

    [1]ARM平台下独占访问指令LDREX和STREX的原理与使用详解

    [2]七、Linux驱动之并发控制

    [3]10.按键之互斥、阻塞机制(详解)

    [4]深入分析Linux自旋锁【转】

    [5]【原创】linux spinlock/rwlock/seqlock原理剖析(基于ARM64)

    [6]Linux中的spinlock机制[一] - CAS和ticket spinlock

    [7]Linux中的spinlock机制[四] - API的使用

  • 相关阅读:
    处理缺失值
    数据清理
    数据聚合
    ajax动态生成table
    MangeEmpHashMap
    Arraylist的雇员管理操作
    jsp获取一个对象和list对象
    Controller比较两个对象discs、outlets中的元素是否相等。相同则相应的checkbox为checked
    限制input text输入的类型(数字,字母,小数点)
    联合主键的映射运用
  • 原文地址:https://www.cnblogs.com/zyly/p/15928982.html
Copyright © 2020-2023  润新知