关于SMP

关于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->SpinCountcount > 0count--)

        {

            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

你可能感兴趣的:(关于SMP)