"Linux Kernel Design and Implementation" Reading Notes-Kernel Synchronization Method

Atomic manipulation

Atomic operations are the cornerstone of other synchronization methods.

The kernel provides two sets of atomic operation interfaces, which are for integer operations and individual bit operations.

Atomic operations on integers can only process data of type atomic_t (include\linux\types.h).

typedef struct {
    volatile int counter;
} atomic_t;

There is also a 64-bit version, but it's almost the same.

There is no special data type for atomic operations on bits. It accepts two parameters, a memory address pointer, and a bit number.

Bit atomic operation function:

There are also non-atomic bit manipulation functions, whose function names are added __ in front of the function names in the above table.

Integer atomic operation functions:

 

Spin lock

The most common lock in the Linux kernel is a spin lock.

A contended spin lock causes the thread requesting it to spin while waiting for the lock to be available again, which wastes processor time.

This leads to the best use of spin in short-term, lightweight situations.

The realization of spin lock is closely related to the system. one example:

static DEFINE_SPINLOCK(i8259_irq_lock);
inline void
i8259a_enable_irq(unsigned int irq)
{
    spin_lock(&i8259_irq_lock);
    i8259_update_irq_hw(irq, cached_irq_mask &= ~(1 << irq));
    spin_unlock(&i8259_irq_lock);
}

On single-processor machines, spin locks are not added when compiling. Only when the kernel preemption mechanism is set, will the spin-compilation option be considered.

Spin locks are not recursive. After holding the spin lock, you can't wait for the spin lock, otherwise it will deadlock.

The spin lock can be used in the interrupt handler, but the local interrupt must be disabled before acquiring the lock, otherwise, the interrupt handler will interrupt the kernel code that is holding the lock, and it may view the contention that has been held. Some spin locks.

The kernel provides a function to disable interrupts while requesting a lock:

static long
iommu_arena_alloc(struct device *dev, struct pci_iommu_arena *arena, long n,
          unsigned int align)
{
    unsigned long flags;
    unsigned long *ptes;
    long i, p, mask;
    spin_lock_irqsave(&arena->lock, flags);
    /* 中间略 */
    spin_unlock_irqrestore(&arena->lock, flags);
    return p;
}

When the lower part shares data with the process context, the shared data interrupted by the process context must be protected, so the execution of the lower part must be prohibited while locking.

When the interrupt handler and the lower half share data, it is necessary to obtain the appropriate lock while also prohibiting the interrupt.

Sometimes the purpose of locks can be clearly divided into two scenarios: read and write. The test can use a read-write spin lock. It is an optimization of spin locks in some cases.

Sleep is not allowed while holding a spin lock.

Spin lock operation function:

 

signal

If the lock time is not long and the code will not sleep, using a spin lock is the best choice; if the lock time may be long or the code may go to sleep when the code holds the lock, it is best to use a semaphore to complete the lock jobs.

Semaphore is a kind of sleep lock.

Semaphore has more overhead than spin lock.

You can go to sleep while holding the semaphore.

While occupying the semaphore, you cannot occupy the spin lock, because the former can sleep, but the latter cannot.

The code holding the semaphore can be preempted.

The semaphore can allow any number of lock holders at the same time. If there is at most one, it is called a mutually exclusive semaphore, and if there is more than one, it is called a counting semaphore.

Usually use mutually exclusive semaphores.

Semaphore has two atomic operations (include\linux\semaphore.h):

extern void down(struct semaphore *sem);
extern void up(struct semaphore *sem);

The down() operation requests a semaphore by subtracting 1 from the semaphore count. If the result is 0 or greater than 0, the semaphore lock is obtained.

The up() operation is used to release the semaphore.

Initialization of the semaphore:

static inline void sema_init(struct semaphore *sem, int val)
{
    static struct lock_class_key __key;
    *sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
    lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}

as well as

#define init_MUTEX(sem)     sema_init(sem, 1)

Semaphores are usually created dynamically as part of a large data structure.

An example of semaphore usage:

int dma_free_channel(DMA_Handle_t handle    /* DMA handle. */
    ) {
    int rc = 0;
    DMA_Channel_t *channel;
    DMA_DeviceAttribute_t *devAttr;
    if (down_interruptible(&gDMA.lock) < 0) {
        return -ERESTARTSYS;
    }
         // ...
out:
    up(&gDMA.lock);
    wake_up_interruptible(&gDMA.freeChannelQ);
    return rc;
}

There are also read and write semaphores.

Semaphore operation function:

 

Mutex (mutex)

Mutex is a simpler sleep lock than semaphore.

It is a simplified version of semaphore.

Static initialization (include\linux\mutex.h):

#define __MUTEX_INITIALIZER(lockname) \
        { .count = ATOMIC_INIT(1) \
        , .wait_lock = __SPIN_LOCK_UNLOCKED(lockname.wait_lock) \
        , .wait_list = LIST_HEAD_INIT(lockname.wait_list) \
        __DEBUG_MUTEX_INITIALIZER(lockname) \
        __DEP_MAP_MUTEX_INITIALIZER(lockname) }
#define DEFINE_MUTEX(mutexname) \
    struct mutex mutexname = __MUTEX_INITIALIZER(mutexname)

Dynamic initialization:

# define mutex_init(mutex) \
do {                            \
    static struct lock_class_key __key;     \
                            \
    __mutex_init((mutex), #mutex, &__key);      \
} while (0)

Locking and unlocking of mutexes:

struct clk *clk_get_sys(const char *dev_id, const char *con_id)
{
    struct clk *clk;
    mutex_lock(&clocks_mutex);
    clk = clk_find(dev_id, con_id);
    if (clk && !__clk_get(clk))
        clk = NULL;
    mutex_unlock(&clocks_mutex);
    return clk ? clk : ERR_PTR(-ENOENT);
}

Only one task can hold mutex at any time;

Locking mutex must be responsible for unlocking it;

Locking and unlocking recursively is not allowed;

When holding a mutex, the process cannot exit;

Mutex cannot be used in the interrupt or the lower half;

Mutex can only be managed through official API;

Compared to semaphores, mutex should be used in preference.

Mutex operation function:

Comparison of spin lock and mutex:

 

Completion variable

A task in the kernel needs to send a signal to notify another task that an event has occurred, and the completion variable can be used.

The completion variable has completion (include\linux\completion.h):

struct completion {
    unsigned int done;
    wait_queue_head_t wait;
};

Initial use:

#define DECLARE_COMPLETION(work) \
    struct completion work = COMPLETION_INITIALIZER(work)

Complete variable operation function:

 

Sequential lock (seq lock)

Sequence locks provide a simple mechanism for reading and writing shared data.

This lock relies on a sequence counter.

When shared data is written, a lock will be obtained and the sequence value will increase; before and after the data is read, the sequence value will be read. If the read sequence number value is the same, it means that the read operation is in progress Has not been interrupted by write operations.

Structure representation of seq lock (include\linux\seqlock.h):

typedef struct {
    unsigned sequence;
    spinlock_t lock;
} seqlock_t;

Define a seq lock:

__cacheline_aligned_in_smp DEFINE_SEQLOCK(xtime_lock);
#define DEFINE_SEQLOCK(x) \
        seqlock_t x = __SEQLOCK_UNLOCKED(x)

Use of write lock:

write_seqlock(&xtime_lock);
do_something();
write_sequnlock(&xtime_lock);

The use of read lock:

u64 get_jiffies_64(void)
{
    unsigned long seq;
    u64 ret;
    do {
        seq = read_seqbegin(&xtime_lock);
        ret = jiffies_64;
    } while (read_seqretry(&xtime_lock, seq));
    return ret;
}

Selection of seq lock:

  • There are many readers of your data;
  • You have few data writers;
  • Although there is little writing, you want to write prior to reading, and don’t allow readers to starve writers;
  • Your data is simple, but atomic weights cannot be used;

The jiffies above is an example.

 

Order and barrier

The program code needs to issue read and write instructions in a specified order. However, in order to improve efficiency, the compiler and processor may reorder the types of read and write instructions, causing exceptions.

In order to use instructions to ensure order, such instructions are called barriers.

rmb() reads the memory barrier.

wmb() write memory barrier.

There is also an mb() that also provides a read-write barrier.

In x MB before () loading operation is not rearranged after the call, in the same way x loaded MB () after the operation will not be rearranged prior to the call.

Barrier method:

 

Kernel preemption related functions

If sharing data is unique to each processor, locks may not be needed.

You can use preempt_disable() to disable kernel preemption.

 

Guess you like

Origin blog.csdn.net/jiangwei0512/article/details/106148682