The sequence lock of Linux synchronization primitives (Sequence Lock)

Sequence lock gives a higher priority to the writer, allowing the writer to continue to run even when the reader is reading. The advantage of this strategy is that the writer never waits. The disadvantage is that sometimes the reader has to read the same data repeatedly until it obtains a valid copy.

In the Linux kernel code, the sequence lock is defined as the seqlock_t structure (the code is located in include/linux/seqlock.h):

typedef struct { struct seqcount seqcount; spinlock_t lock; } seqlock_t; Therefore, it contains a spin lock lock and a seqcount that represents the sequence number of the current lock. The seqcount structure is defined as:



typedef struct seqcount { unsigned sequence; } seqcount_t; is a sequence variable that contains an unsigned integer.



initialization

Sequence lock needs to be initialized before use. There are generally two methods:

DEFINE_SEQLOCK(lock1);

seqlock_t lock2;
seqlock_init(&lock2); As
you can see, the first method is to directly define and initialize a sequence lock variable with a macro:

#define SEQCNT_ZERO(lockname) {.sequence = 0, …}

#define __SEQLOCK_UNLOCKED(lockname)
{
.seqcount = SEQCNT_ZERO(lockname),
.lock = __SPIN_LOCK_UNLOCKED(lockname)
}

#define DEFINE_SEQLOCK(x)
seqlock_t x = __seqlock_t x = x)
Therefore, directly through the macro definition initialization is to define a seqlock_t structure variable, initialize the internal spin lock lock to be unlocked, and initialize the seqcount variable representing the sequence number to 0.

The second method is to define a seqlock_t structure variable yourself, and then call the seqlock_init function to initialize it:

static inline void __seqcount_init(seqcount_t *s, const char *name,
struct lock_class_key *key)
{ s->sequence = 0; } #define seqcount_init(s) __seqcount_init(s, NULL, NULL) #define seqlock_init(x ) do { seqcount_init(&(x)->seqcount); spin_lock_init(&(x)->lock); } while (0) also initializes the internal spin lock variable lock, and initializes the seqcount variable representing the sequence number to 0.











Write operation

Sequence lock distinguishes writers and readers. For writers, the following usages are generally used:

write_seqlock(&seq_lock);
/* Modify data*/

write_sequnlock(&seq_lock);
Just sandwich the code for modifying data between write_seqlock and write_sequnlock functions.

The write_seqlock function to obtain the write sequence lock is defined as follows:

static inline void write_seqlock(seqlock_t sl)
{ /
Get spin lock*/
spin_lock(&sl->lock);
write_seqcount_begin(&sl->seqcount);
}
First obtain the spin lock inside the sequence lock, and then call the write_seqcount_begin function:

static inline void write_seqcount_begin(seqcount_t *s)
{
write_seqcount_begin_nested(s, 0);
}
接着调用write_seqcount_begin_nested函数:

static inline void write_seqcount_begin_nested(seqcount_t *s, int subclass)
{ raw_write_seqcount_begin(s); } Finally, the raw_write_seqcount_begin function is called:



static inline void raw_write_seqcount_begin(seqcount_t s)
{ /
accumulated sequence number /
s->sequence++;
/
write memory barrier*/
smp_wmb();
}
accumulates the sequence number in the sequence lock, after which a write memory barrier is added. This is to ensure that other modules in the system can perceive that the sequence number has been accumulated before the modified data code after the write_seqlock function is officially executed. That is to ensure that the instructions of the cumulative sequence number will not be reordered to the subsequent modified data code, otherwise, it is possible that the code that modifies the data code has been executed a bit, and other CPUs have not yet sensed that the sequence number has been changed. Cause inconsistent reading data. Of course, you should also add a corresponding read memory barrier when reading.

The function of the write_sequnlock function that releases the write sequence lock is basically to reverse the process of obtaining the lock, which is defined as follows:

static inline void write_sequnlock(seqlock_t sl)
{ write_seqcount_end(&sl->seqcount); /

release spin lock*/
spin_unlock(&sl->lock);
}
first call write_seqcount_end function, and then release the spin lock:

static inline void write_seqcount_end(seqcount_t *s)
{

raw_write_seqcount_end(s);
}
接着调用raw_write_seqcount_end函数:

static inline void raw_write_seqcount_end(seqcount_t s)
{ /
write memory barrier /
smp_wmb();
/
accumulate sequence number*/
s->sequence++;
}
Add write memory barrier first, and then accumulate sequence number. This is to ensure that the sequence number can only be accumulated after the code that modifies the data is executed. Therefore, the write memory barrier used in the previous write_seqlock and the write memory barrier used here in write_sequnlock are paired, forming a critical section to perform data modification operations.

At the same time, since the sequence number of the sequence lock is initialized to 1, the sequence number will be increased by 1 when the write lock is locked, and 1 will still be added when the write lock is opened, so when the sequence number is read, it must be indicated One writer has acquired the write sequence lock, and when the read sequence number is even, it must indicate that no writer currently has acquired the write sequence lock.

Moreover, for different writers, the serial lock is protected by a spin lock, so there can only be one writer at a time.

Finally, very critically, the write sequence lock will not cause the current process to sleep.

Read operation

Next, we analyze the readers of sequential locks. For readers, generally use the following usage:

unsigned int seq;

do { seq = read_seqbegin(&seq_lock); /* read data*/ } while read_seqretry(&seq_lock, seq); generally use the read_seqbegin function to read the sequence number of the sequence lock, and then perform the actual read data operation , And finally call the read_seqretry function to see if the sequence number of the current sequence lock is consistent with the sequence lock read earlier. If they are consistent, it proves that no writer is writing during the reading process, and you can exit directly; if they are inconsistent, it means that at least one writer has modified the data during the reading process, and then repeat the above steps in a loop , Until the sequence numbers read before and after are consistent.




The read_seqbegin function that reads the current sequence lock sequence number is defined as follows:

static inline unsigned read_seqbegin(const seqlock_t *sl)
{
return read_seqcount_begin(&sl->seqcount);
}
调用了raw_read_seqcount_begin函数:

static inline unsigned read_seqcount_begin(const seqcount_t *s)
{ return raw_read_seqcount_begin(s); } Then the raw_read_seqcount_begin function is called:



static inline unsigned raw_read_seqcount_begin(const seqcount_t s)
{ /
read sequence number /
unsigned ret = __read_seqcount_begin(s);
/
read memory barrier*/
smp_rmb();
return ret;
}
First call __read_seqcount_begin function to read the current sequence lock The sequence number is then added with a read memory barrier.

static inline unsigned __read_seqcount_begin(const seqcount_t *s)
{
unsigned ret;

repeat:
/* The sequence number of the read sequence lock /
ret = READ_ONCE(s->sequence);
/
If the sequence number is odd, it means that a writer is writing /
if (unlikely(ret & 1)) { /
loop waiting /
cpu_relax();
goto repeat;
}
/
Return the sequence number until it is an even number*/
return ret;
}
Read the sequence number of the sequence lock first, and add READ_ONCE to prevent the compiler from optimizing it and the subsequent conditional judgments. Out of order of execution. Then, determine whether the sequence number is an odd number. As mentioned earlier, if it is an odd number, it means that a writer is holding a write sequence lock. At this time, call the cpu_relax function to give up control of the CPU, and read the sequence again from the beginning. Count until it is even.

The cpu_relax function is implemented by each architecture itself. The implementation of the Arm64 architecture is as follows (the code is located in arch/arm64/include/asm/processor.h):

static inline void cpu_relax(void)
{ asm volatile("yield" ::: "memory"); } In most Arm64 implementations, the yield instruction is equivalent to the nop null instruction. It just tells the current CPU core that the currently executing thread has nothing to do, and the current CPU core can do something else. Usually, this kind of instruction is only useful for CPU cores that support hyper-threading, but all current Arm64 implementations do not support hyper-threading technology, so they are only processed as empty instructions.


Next, let's take a look at the implementation of the read_seqretry function that determines whether the sequence number of the current sequence lock is consistent with the sequence lock read earlier:

static inline unsigned read_seqretry(const seqlock_t *sl, unsigned start)
{
return read_seqcount_retry(&sl->seqcount, start);
}
调用了read_seqcount_retry函数:

static inline int read_seqcount_retry(const seqcount_t s, unsigned start)
{ /
read memory barrier*/
smp_rmb();
return __read_seqcount_retry(s, start);
}
First add a read memory barrier, and the read used in the previous read_seqbegin Memory barriers are paired and form a critical section for performing operations to read data. Then, the __read_seqcount_retry function is called:

static inline int __read_seqcount_retry(const seqcount_t *s, unsigned start)
{ return unlikely(s->sequence != start); } It simply judges whether the sequence number of the current sequence lock is consistent with the value of the incoming start parameter.


Readers are not protected by a spin lock, so multiple readers can read data at the same time, and the read sequence lock will not cause the current process to sleep.

scenes to be used

Sequence lock is not a panacea, and the use scenarios suitable for it must meet the following conditions:

It is more suitable for scenarios with more reading and less writing. When analyzing the code earlier, I saw that the writer is protected by a spin lock, so only one writer can write data at a time, and the reader is not protected by any other locks and reads concurrently. Therefore, the original writing performance is not high, and readers must ensure that no writers will write during the entire period of reading data. If there are many writers, they will keep retrying to read, which will seriously affect performance.
The data to be protected is generally not too much, otherwise it will affect performance.
The protected data structure does not include pointers modified by the writer and indirectly referenced by the reader. Otherwise, the writer may invalidate the pointer while the reader is reading the data pointed to by the pointer.
The reader's critical section code has no other operations that cause other side effects besides reading data. Otherwise, the operations of multiple readers will compete with each other. This is because readers of sequential locks are not protected by any other locks, and everyone reads concurrently, but simply uses a pair of read memory barriers to protect them.
Sequence lock will not cause readers and writers to sleep.
The most common, in the Linux kernel, updating system jiffies is the sequential lock used.

Note: Need C/C++ Linux Advanced Server Architect learning materials plus qun: 812855908 (data includes C/C++, Linux, golang technology, Nginx, ZeroMQ, MySQL, Redis, fastdfs, MongoDB, ZK, streaming media, CDN, P2P, K8S, Docker, TCP/IP, coroutine, DPDK, ffmpeg, etc.), free to shareInsert picture description here

Guess you like

Origin blog.csdn.net/qq_40989769/article/details/107715048