关于SMP
对称式多处理器(Symmetric Multi-Processor),缩写为SMP,是一种计算机系统结构。多处理器结构有两种:
l 对称 —— 多个处理器都是等价的,线程每次受调度运行时都可以动态选择在任何一个处理器上运行。
l 非对称 —— 处理器的结构、能力、所处的部位、和作用都各不相同,不同的线程只能在特定的处理器上运行。
如果一个软件可以在SMP结构的计算机上正常运行,就称为“SMP安全” (“SMP Safe”)。
操作系统是整个系统的核心,所以操作系统是否“SMP安全”,或者说是否支持SMP,就是个关键性的问题。
那么什么样的软件才是“SMP安全”的呢?
软件是作为线程在操作系统上运行的,而线程每次受调度运行时又都可以动态选择在SMP结构中的任何一个处理器上运行,软件之是否“SMP安全”,实际上取决于线程之间的互相作用。下面的讨论都以两个线程T1和T2为例,但实际上也适用于更多个线程。
首先,假定两个线程分属两个不同的进程,并且互相之间也没有共享内存、不访问共同的外设,那么它们就是“井水不犯河水”,各做各的事情,跟系统是否SMP结构无关,所以天然就是“SMP安全”的。即使两个线程在执行相同的代码,也是如此(只要在运行过程中不会修改代码段就行)。
总之,一般而言,只有可以被多个线程共享的变量才有冲突,因而才需要“互斥”,即在一个线程访问共享变量时不允许别的线程前来干扰。
如果两个线程属于同一个进程,那么由于共享同一个用户空间,互相之间就可能有冲突了。引起这种冲突的原因可能有:
l 全局变量(包括static局部量),或者更确切地说是“共享量”。
l 队列
l 共享内存
下面举例说明这个问题。
假定线程T1和T2同时执行到了下面这个函数,而只有细微的先后差别。
struct msg_queue *create_msg_queue(struct w32thread *thread, struct thread_input *input)
{
struct msg_queue *queue;
……
status = create_object(KernelMode, ……, (PVOID *)&queue);
if (NT_SUCCESS(status) && queue) {
……
thread->queue = queue;
……
}
……
return queue;
}
这里的thread形式上不是全局量,但实质上是。这是因为,内核中关于线程的数据结构是全局的,具体线程的struct w32thread数据结构并不是只有本线程才能访问,别的线程也有可能访问它。比方说:
struct msg_queue *get_current_queue(void)
{
struct msg_queue *queue = current_thread->queue;
if (!queue)
queue = create_msg_queue(current_thread, NULL);
……
return queue;
}
这里访问的是本线程自身的数据结构。然而:
int attach_thread_input(struct w32thread *thread_from, struct w32thread *thread_to)
{
……
if (!thread_to->queue && !(thread_to->queue = create_msg_queue(thread_to, NULL)))
return 0;
……
return 1;
}
这里的thread_to却是另一个线程的数据结构。所以T1和T2完全有可能从不同的路径同时进入create_msg_queue()这个函数,而赋值语句“thread->queue = queue”中的指针thread则指向同一个数据结构。于是,T1和T2在这个函数中各自分配了一个msg_queue数据结构,然后假定T1先完成赋值,然后T2又来赋值,则thread->queue被覆盖,T1所分配的那个数据结构就丢失了。这不但会引起内存泄漏,还可能引起程序中的混乱。当然,之所以如此是因为这里有对于全局量的写操作,如果两个线程都只是读就不会有问题。
但是,回到前面的create_msg_queue(),为什么会有这样的问题,怎样才能避免这样的问题呢?问题在于,对create_msg_queue()的调用应该是有条件的,那就是指针thread->queue为空,正如get_current_queue()的代码所示:
if (!queue)
queue = create_msg_queue(current_thread, NULL);
这个条件语句的执行应该是不可分割的,也就是“原子的”。也就是说,如果一个线程检测到指针queue为空,那就一定要做完了create_msg_queue()并完成赋值以后才可允许别的线程检测这个指针是否为空。否则的话,如果有两个线程“同时”捡测到指针queue为空,那就注定其中之一所创建的队列会丢掉了。
但是对于局部量的访问就一般不存在冲突的问题,因为局部量在堆栈上,不同的线程各有自己的堆栈,所以不会发生冲突。例如,假定T1和T2同时执行到了这里的赋值语句:
int attach_thread_input(struct w32thread *thread_from, struct w32thread *thread_to)
{
struct desktop *desktop;
struct thread_input *input;
……
input = (struct thread_input *)grab_object(thread_to->queue->input);
……
return 1;
}
只要参数thread_to不同,这里grab_object()的返回值就很可能不同,那指针input的值是否有可能被覆盖呢?不会。这是因为T1和T2的局部量input在不同的堆栈上,它们只是同名,但实际上是两个不同的变量。
因此,局部量一般不存在是否“SMP安全”的问题。但是这并不表示局部量就不会有问题,因为有些形式上的局部量实质上是全局量。
队列一般都是共享的,逻辑上相当于全局量,而且队列操作一般都带有写操作,所以队列操作最容易引起这样的冲突。例如:
insert_obdir_entry(
IN POBJECT_DIRECTORY Directory,
IN PVOID Object
)
{
......
HeadDirectoryEntry = Directory->LookupBucket;
……
NewDirectoryEntry = (POBJECT_DIRECTORY_ENTRY)
kmalloc(sizeof(OBJECT_DIRECTORY_ENTRY), GFP_KERNEL);
……
/* insert at the bucket chain head */
NewDirectoryEntry->ChainLink = *HeadDirectoryEntry;
*HeadDirectoryEntry = NewDirectoryEntry;
NewDirectoryEntry->Object = Object;
......
}
这是单链的队列操作。如果T1和T2同时执行到这里,就可能会发生这样的情况:
l 参数Directory指向同一个目录,所以两个线程的HeadDirectoryEntry相等。
l 但是两个线程各自分配了NewDirectoryEntry。
l 两个线程也各有自己的Object。
l 时间上T1比T2略为领先。
l 于是T1的NewDirectoryEntry就丢失了,属于T1的对象Object也没有进入目录。
同样,这里也因丢失数据结构而造成了内存泄漏,并且这样一来程序的逻辑也错了。
简单的单链队列尚且如此,双链就更不用说了。例如:
static inline void list_add_after(struct list_head *elem, struct list_head *to_add)
{
to_add->next = elem->next;
to_add->prev = elem;
elem->next->prev = to_add;
elem->next = to_add;
}
显然,这段程序所实现的操作应该是原子操作,如果这个操作的原子性得不到保证,就肯定会出问题。
所以队列操作是最容易出问题的操作,只要可能有多个线程并发访问同一个队列,就是容易出问题的地方。
那么什么情况下会有多个线程并发访问同一个队列呢?一般而言有这么一些:
一、显式
l 通过fork()为同一段程序生成多个线程。
l 遵循生产者/消费者模型的操作,这里面又有一对一、一对多、多对一、多对多等几种情况。(线程间通信也属于这种模型,不过线程间通信由系统提供,可以认为是安全的)。
l 在内核中,由于队列存在于共享的系统空间,多线程与多进程在这个问题上是等价的。
二、隐式
l 中断服务程序和bh函数都是“盗用”当前线程的上下文执行的,所以应认为系统中所有线程的程序中都包括(所有的)中断服务程序和bh函数。特别要注意的是,由于中断服务程序和bh函数的存在,还可能发生同一个线程嵌套访问一个队列的情况。例如,假定T1正在把一个包挂入双链的发送队列,尚在修改指针的中途,此时发生了一个中断,并因此而需要从发送队列中摘下一个包。由于此时的当前线程就是T1,就发生了T1嵌套访问同一个双链队列,既要挂入一个包、又要摘下一个包的情况。
由此可见,队列操作是十分敏感,很容易出问题的操作
至于共享内存,实质上也相当于全局量,所以也是可能出问题的操作。
上述所有的情景,之所以会有问题,就是因为本来应该作为一个整体的“原子操作”被拆散了,中间插进了针对相同目标的其它操作。也就是说,一些操作对于原子性的要求没有得到满足。
所谓“操作”,是个比较模糊的概念,大到一个程序,小到一条指令,都有可能被看作是一个操作,所以具体的操作有规模大小的问题,我们称之为“粒度”。另一方面,也不是所有的操作都有原子性的要求。
如果有一种有原子性要求的操作、其粒度小到一条指令,只用一条指令就可完成,那么这个操作的原子性要求在单CPU的系统中就是可以满足的。这是因为,在单CPU的系统中,单条指令的执行是不可分割的。所谓不可分割,是说在单条指令的执行过程中不会有别的操作插进来,所以这个操作对于所操作的对象是独占的。相比之下,如果一个操作要由连续两条指令才能完成,那就可能会有别的操作插进来了。
在单CPU的系统中,有别的操作插进来的原因只有一个,那就是中断。注意,单条指令在执行过程中是不会发生中断的,中断只能发生在两条指令之间。如果发生中断,那么插进来的至少有中断服务程序,在多线程的系统中还可能引起调度。而如果引起了调度,那么这中间有多少个线程会插进来就不可预测了。总之,在单CPU的系统中,只要一个操作的粒度小到可以由单条指令完成,那么这个操作的原子性要求就是可以满足的。
可是,在多CPU的系统中,情况又不同了。在多CPU的系统中,即使一个操作可以由单条指令完成,也不能保证其原子性。这是因为此时中断已不再是破坏原子性的唯一原因,更大的问题在于多个CPU对于内存的共享。
下面举个例子加以说明。
X86架构的CPU有条指令XADD,Intel的手册中关于这条指令的定义是这样:
TEMP <= SRC + DEST
SRC <= DEST
DEST <= TEMP
这条指令的操作数DEST是操作的目标,一般是内存中的一个变量;SRC一般也是一个变量,而TEMP则是CPU内部用作草稿的一个寄存器。事实上,这条指令是专门用来实现临界区的,其执行包括三个微操作,需要三个时钟周期(取指令不算在内)。显而易见,如果在指令执行的时候有另一个CPU也对同一个内存单元执行这条指令,并且时间上只相差一个时钟周期,就会发生运行于两个CPU上的两个线程同时进入临界区的错误。
由此可见,在多CPU的系统中,即使能以单条指令完成的操作也不能保证其原子性了,只有粒度小到单个时钟周期程度的操作才能保证其原子性。为此,x86架构的CPU允许在执行某些指令时锁住总线,写程序时可以在汇编指令前面加上前缀LOCK。不过,并非所有的指令都可以加LOCK,而只允许在少数指令上加LOCK,允许加LOCK的指令限于“读-处理-写”模式的操作,主要用来实现临界区。
总之,上述所有的情景,对于单核的多线程操作就有问题,对于多核系统则更有问题。实际上,线程本质上就是对于CPU的虚拟,而单CPU的多线程系统本质上就是虚拟的多CPU系统。但是,在单核的多线程系统中,实际的执行是经过“串行化”的,物理的CPU只有一个,只是在不同的时间中用于不同的线程,两个不同线程不会在同一个物理时间点上发生冲突。而多核系统中的不同CPU,则有可能在同一个物理时间点上发生冲突,所以更难以保证其原子性。
那么,哪一些操作应该是原子的,又怎样才能保证这些操作的原子性呢?
先看哪一些操作应该是原子的:
l 对同一个变量的修改(读出-改变-回写)。
l 队列操作。
l 某些赋值操作。
l 其它需要放在临界区中完成的操作序列。
再考虑怎样保证原子性的问题。
先看在单CPU的多线程系统怎么保证操作的原子性:
在单CPU的多线程系统中,要保证用户空间的操作的原子性,就只有两个办法:
l 放在同一条指令中完成。
l 放在临界区中完成。
对于用户空间的程序,要切记:任何两条指令之间都可以发生中断和线程切换。因此,只要一个原子操作的粒度超过单条指令,就一定要放在临界区中。另一方面,只要把操作放在了临界区中,一般而言就是安全的。
而如果是在内核中,那么情况有所不同:
l 在内核中,并非任何两条指令之间都可以发生中断,并可以在程序中关中断,形成一个“中断禁区”。中断禁区中既然不会发生中断,则自然也不会发生(被动的)调度,所以同时又是“调度禁区”。
l 即使发生中断,也不一定可以发生线程切换,具体取决于调度策略:
1. 如果是实时调度,那么应该认为任何两条指令之间都可以发生线程切换(实际上在中断服务程序里面是不会发生切换的)。
2. 如果是非实时调度,那么基本上不会发生线程切换。
l 除调度策略的影响外,在程序中还可以禁止调度,形成一个“调度禁区”。在调度禁区中仍可发生中断,但不会发生调度。
l 有些原子操作光是放在普通的临界区中还不够,还需要把中断关掉。例如,假定中断服务程序从网卡读包,并将包挂入接收队列;而应用程序通过系统调用从接收队列摘包,那就要用“临界区+关中断”的方案解决。
由此可见,不管是用户空间还是系统空间,临界区是个保证操作原子性的基本手段。除临界区外,在内核中也可以用调度禁区和中断禁区保证操作的原子性。其中调度禁区可以防止不同线程之间的冲突,而中断禁区可以防止(因中断引起的)同一线程的自相冲突。
至于临界区的实现,在单CPU和多CPU系统中又有不同的考虑。
在单CPU的系统中,临界区要防止的是不同线程针对共享变量的操作的穿插和混合,以实现有序的串行化。这决定了:
一时不能进入临界区的线程必须让出CPU(这是系统中唯一的CPU),使已经进入临界区的线程有机会完成其操作并退出临界区。换言之,在进不了临界区的时候必须睡眠等待。与此相应,每当有线程从临界区退出时必须唤醒可能正在睡眠等待的线程。
但是,在多CPU的系统中,则有所不同:
l 不存在必须让出CPU的问题,因为有多个CPU。
l 要让出CPU就要用到睡眠/唤醒的机制,既有系统开销代价可能太大的问题,也有时间延迟的问题,具体要看(临界区中)原子操作的大小。
l 实际上原子操作一般都是不大的,让一时不能进入临界区的线程空转等待往往更好。如果采用空转等待,那就是“空转锁”、即Spinlock。
所以:
1. Spinlock也是用来实现临界区的,但只可用于SMP结构的系统;
2. 而普通临界区既可用于单CPU系统,也可用于SMP系统。
另一方面:
1. 单CPU系统只可以用普通临界区,而不能用Spinlock。
2. SMP系统二者都可以用,但是Spinlock的效率较高。
不过,Spinlock是一种特殊的临界区。它的特殊之处在于:对于在不同CPU上运行的线程,它是临界区;而对于同一个CPU上的线程,则是调度禁区或中断禁区。或者说,它是跨CPU的临界区,又是同一CPU上的调度禁区或中断禁区。这样,在多CPU的系统上,Spinlock首先表现为调度禁区(或中断禁区),同时它又是个临界区,总之就是在同一时间内只有一个线程可以进入这个禁区和临界区。而在单CPU的系统上,则Spinlock就是一个调度禁区(或中断禁区),由于是调度禁区,就不会有(同一CPU上的)其它线程进入,所以临界区就形同虚设、名存实亡。这就保证了在单CPU系统上任何线程都不会被拦在Spinlock临界区之外,因为根本就不存在竞争。正因为这样,Spinlock既可用于多CPU系统,起着调度禁区加临界区的作用;也可用于单CPU系统,此时只起调度禁区的作用。
另外还有个办法,就是采用Spinlock和普通临界区的结合,就是不能进入临界区的线程先空转上几圈,如果转上几圈还是进不去就转入睡眠,Wine代码中用来实现临界区的EnterCriticalSection()就是这样。
在Wine的代码中,EnterCriticalSection()实际上是RtlEnterCriticalSection(),这一点从kernell32的spec文件中可以看出:
@ stdcall EnterCriticalSection(ptr) ntdll.RtlEnterCriticalSection
RtlEnterCriticalSection()的代码在ntdll/critsection.c中:
NTSTATUS WINAPI RtlEnterCriticalSection( RTL_CRITICAL_SECTION *crit )
{
if (crit->SpinCount)
{
ULONG count;
if (RtlTryEnterCriticalSection( crit )) return STATUS_SUCCESS;
for (count = crit->SpinCount; count > 0; count--)
{
if (crit->LockCount > 0) break; /* more than one waiter, don't bother spinning */
if (crit->LockCount == -1) /* try again */
{
if (interlocked_cmpxchg( &crit->LockCount, 0, -1 ) == -1) goto done;
}
small_pause();
}
}
if (interlocked_inc( &crit->LockCount ))
{
if (crit->OwningThread == ULongToHandle(GetCurrentThreadId()))
{
crit->RecursionCount++;
return STATUS_SUCCESS;
}
/* Now wait for it */
RtlpWaitForCriticalSection( crit );
}
done:
crit->OwningThread = ULongToHandle(GetCurrentThreadId());
crit->RecursionCount = 1;
return STATUS_SUCCESS;
}
我们知道,Wine的代码分两部分,一部分是DLL,另一部分是Wine Server。前者已经考虑到对于SMP的支持,凡有原子性要求的操作都已用EnterCriticalSection()和LeaveCriticalSection()加以保护,而EnterCriticalSection()就是RtlEnterCriticalSection()。
而Wine Server,则原来的设计是一台机器上只有一个服务线程,而服务线程的操作又是在主循环中每次处理一个请求,是完全串行化的,因而天然就是原子性的,所以不需要用临界区加以保护。但是,移到内核中以后就不一样了。原来的主循环已不复存在,对请求的处理转化成系统调用,分散到了各个线程自己的上下文中,所以现在的module_2.6.34是完全不支持SMP的,这就是我们下一步要解决的问题。
下面,我们看一下module_2.6.34的代码中一些有问题的操作。
一、在sock/sock.c中:
struct sock
{
.....
struct list_head accentry; /* entry in the list below for the request */
struct list_head paccepts; /* pending accepts on this socket */
struct async_queue *read_q; /* queue for asynchronous reads */
struct async_queue *write_q; /* queue for asynchronous writes */
};
Case A1:
if (!sock->read_q && !(sock->read_q = create_async_queue(sock->fd))) ...
假定有两个线程T1和T2分别在两个CPU上运行,针对同一个sock,同时执行到了这个地方但T1略微领先,那么T1所分配的async_queue就会因指针被覆盖而丢失,既造成混乱又造成内存泄露。
同理,sock->write_q 也是一样。
Case A2:
if (!(async = create_async(current_thread, queue, data))) ...
struct async *create_async(struct w32thread *thread, struct async_queue *queue, const async_data_t *data)
{
......
list_add_before(&queue->queue, &async->queue_entry);
......
}
如果两个线程T1和T2同时运行到这儿,几乎同时进入list_add_before(),就会把队列搞乱。
Case A3:
LIST_FOR_EACH_ENTRY( acceptsock, &sock->paccepts, struct sock, accentry ) ...
LIST_FOR_EACH_ENTRY_SAFE(acceptsock, next, &sock->paccepts, struct sock, accentry) ...
#define LIST_FOR_EACH_ENTRY(elem, list, type, field) \
for ((elem) = LIST_ENTRY((list)->next, type, field); \
&(elem)->field != (list); \
(elem) = LIST_ENTRY((elem)->field.next, type, field))
#define LIST_FOR_EACH_ENTRY_SAFE(cursor, cursor2, list, type, field) \
for ((cursor) = LIST_ENTRY((list)->next, type, field), \
(cursor2) = LIST_ENTRY((cursor)->field.next, type, field); \
&(cursor)->field != (list); \
(cursor) = (cursor2), \
(cursor2) = LIST_ENTRY((cursor)->field.next, type, field))
在这两种情况下,指针acceptsock最初指向队列头sock->paccepts,然后逐个节点推进。如果T1正在推进中T2正好在进行插入/删除,那就可能造成混乱。所以,只要有对队列造成改变的操作正在进行,就应该禁止扫描队列。
Case A4:
list_add(&acceptsock->accentry, &dest_sock->paccepts);
这里的两个参数,一个是队列中的节点,另一个是要进入队列的新节点。如果两个线程T1和T2同时运行到这儿,几乎同时进入list_add,要在同一个节点后面挂上新的节点,就会造成混乱,并造成内存泄漏。
进一步,假定T1要在节点N4后面挂上一个节点,但是恰好T2要删除T2,两个操作同时发生,就更乱套了。
Case A5:
list_remove(&acceptsock->accentry);
参考Case4,但更复杂,因为删除节点涉及3个节点。
二、在ob/object.c中
Case B1:
insert_obdir_entry(
IN POBJECT_DIRECTORY Directory,
IN PVOID Object
)
{
......
/* insert at the bucket chain head */
NewDirectoryEntry->ChainLink = *HeadDirectoryEntry;
*HeadDirectoryEntry = NewDirectoryEntry;
NewDirectoryEntry->Object = Object;
......
}
这是单链的队列操作。如果T1和T2同时执行到这里,就同样会因丢失数据结构
而造成内存泄露。
Case B2:
lookup_obdir_entry(
IN POBJECT_DIRECTORY Directory,
IN PUNICODE_STRING Name,
IN ULONG Attributes
)
{
......
*HeadDirectoryEntry = DirectoryEntry->ChainLink;
DirectoryEntry->ChainLink = *(Directory->LookupBucket);
*(Directory->LookupBucket) = DirectoryEntry;
......
}
Case B3:
delete_obdir_entry (
IN POBJECT_DIRECTORY Directory
)
{
......
HeadDirectoryEntry = Directory->LookupBucket;
......
DirectoryEntry = *HeadDirectoryEntry;
......
*HeadDirectoryEntry = DirectoryEntry->ChainLink;
DirectoryEntry->ChainLink = NULL;
......
}
三、在msg/queue.c中
Case C1:
struct msg_queue *create_msg_queue(struct w32thread *thread, struct thread_input *input)
{
......
thread->queue = queue;
......
return queue;
}
Case C2:
static int merge_message(struct thread_input *input, const struct message *msg)
{
struct message *prev;
struct list_head *ptr = list_tail(&input->msg_list);
if (!ptr)
return 0;
prev = LIST_ENTRY(ptr, struct message, entry);
......
}
Case C3:
void timer_callback(void *private)
{
......
list_remove(ptr);
list_add_before(&queue->expired_timers, ptr);
set_next_timer(queue);
}
注:async_set_result()和timer_callback()都调用thread_queue_apc(),可是thread_queue_apc()里面是ktrace("WHA!! please use NtQueueApcThread\n"),这个问题要仔细看一下。
struct timer
{
struct list_head entry; /* entry in timer list */
timeout_t when; /* next expiration */
unsigned int rate; /* timer rate in ms */
user_handle_t win; /* window handle */
unsigned int msg; /* message to post */
unsigned long id; /* timer id */
unsigned long lparam; /* lparam for message */
};
以上只是一个很不完全的统计,下面我们的任务就是仔细看代码,把所有可能引起问题的操作挑出来。
挑出来之后怎么办呢?一般来说就是用spinlock把原子操作保护起来。下面是Linux内核代码中的一些实例,可供参考。
例一、
static LIST_HEAD(all_bdevs)
static __cacheline_aligned_in_smp DEFINE_SPINLOCK(bdev_lock);
long nr_blockdev_pages(void)
{
struct block_device *bdev;
long ret = 0;
spin_lock(&bdev_lock);
list_for_each_entry(bdev, &all_bdevs, bd_list) {
ret += bdev->bd_inode->i_mapping->nrpages;
}
spin_unlock(&bdev_lock);
return ret;
}
例二、
static void rs_close(struct tty_struct *tty, struct file * filp)
{
spin_lock(&timer_lock);
if (tty->count == 1)
del_timer_sync(&serial_timer);
spin_unlock(&timer_lock);
}
例三、
static void mousedev_attach_client(struct mousedev *mousedev,
struct mousedev_client *client)
{
spin_lock(&mousedev->client_lock);
list_add_tail_rcu(&client->node, &mousedev->client_list);
spin_unlock(&mousedev->client_lock);
synchronize_rcu();
}
例四、
static int fionbio(struct file *file, int __user *p)
{
......
spin_lock(&file->f_lock);
if (nonblock)
file->f_flags |= O_NONBLOCK;
else
file->f_flags &= ~O_NONBLOCK;
spin_unlock(&file->f_lock);
return 0;
}
例五、
static int tc35815_poll(struct napi_struct *napi, int budget)
{
......
spin_lock(&lp->rx_lock);
......
......
spin_unlock(&lp->rx_lock);
......
return received;
}
Linux内核中spinlock的实现
最后我们看一下Linux内核中的spinlock究竟是怎么实现的。先看数据结构:
typedef struct spinlock {
union {
struct raw_spinlock rlock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
struct {
u8 __padding[LOCK_PADSIZE];
struct lockdep_map dep_map;
};
#endif
};
} spinlock_t;
显然,如果忽略DEBUG的需要,则spinlock_t实际上就是struct raw_spinlock。
typedef struct raw_spinlock {
arch_spinlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAK
unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
unsigned int magic, owner_cpu;
void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} raw_spinlock_t;
同样,如果忽略DEBUG的需要,也不要LOCKBREAK,那么struct raw_spinlock实际上就是arch_spinlock_t。
typedef struct arch_spinlock {
unsigned int slock;
} arch_spinlock_t;
由此可见,如果忽略DEBUG和LOCKBREAK的需要,一个spinlock实际上就是一个无符号整数。
在内核中,需要定义一个spinlock时可以这样:
#define DEFINE_SPINLOCK(x) spinlock_t x = __SPIN_LOCK_UNLOCKED(x)
再看include/linux/spinlock.h中spin_lock()的代码:
static inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}
#define raw_spin_lock(lock) _raw_spin_lock(lock)
而_raw_spin_lock又定义成__raw_spin_lock:
#define _raw_spin_lock(lock) __raw_spin_lock(lock)
下面就是__raw_spin_lock()的实现:
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable(); //禁止调度,形成调度禁区。
//这个调度禁区要到程序调用spin_unlock()
//的时候才会通过preempt_enable()加以解除。
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
这里的宏操作会调用do_raw_spin_trylock ()和do_raw_spin_lock(),我们看后者:
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
__acquire(lock);
arch_spin_lock(&lock->raw_lock);
}
到了arch_spin_lock(),具体的实现就因CPU而异了。对于x86架构是这样:
static __always_inline void arch_spin_lock(arch_spinlock_t *lock)
{
__ticket_spin_lock(lock);
}
系统中的CPU数量小于256时,这个函数是这样实现的:
#if (NR_CPUS < 256)
static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock)
{
short inc = 0x0100;
asm volatile (
LOCK_PREFIX "xaddw %w0, %1\n"
"1:\t"
"cmpb %h0, %b0\n\t"
"je 2f\n\t"
"rep ; nop\n\t"
"movb %1, %b0\n\t"
/* don't need lfence here, because loads are in-order */
"jmp 1b\n"
"2:"
: "+Q" (inc), "+m" (lock->slock)
:
: "memory", "cc");
}
#else
#endif
注意这里用了指令XADD。当进不了临界区的门时,CPU执行的是“rep; nop”,消磨掉一点点时间后又转回标号1,这就是“spin”的意思。
除spin_lock()以外,Linux内核中还有些类似的函数,其中spin_lock_irq()有着特别的重要性。
spin_lock_bh()
spin_lock_irq()
spin_lock_nested()
spin_lock_irqsave()
spin_lock_irqsave_nested()
可想而知,如果当前线程正在对一个队列进行操作,而某个中断服务程序中也可能会对此队列进行操所,那就应该用spin_lock_irq()。其它就不多说了。
网址来源:http://www.longene.org/techdoc/0409875001314757116.html