关于SMP

对称式多处理器(Symmetric Multi-Processor),缩写为SMP,是一种计算机系统结构。多处理器结构有两种:

对称 —— 多个处理器都是等价的,线程每次受调度运行时都可以动态选择在任何一个处理器上运行。

非对称 —— 处理器的结构、能力、所处的部位、和作用都各不相同,不同的线程只能在特定的处理器上运行。

如果一个软件可以在SMP结构的计算机上正常运行,就称为“SMP安全” (“SMP Safe”)。

操作系统是整个系统的核心,所以操作系统是否“SMP安全”,或者说是否支持SMP,就是个关键性的问题。

那么什么样的软件才是“SMP安全”的呢?

软件是作为线程在操作系统上运行的,而线程每次受调度运行时又都可以动态选择在SMP结构中的任何一个处理器上运行,软件之是否“SMP安全”,实际上取决于线程之间的互相作用。下面的讨论都以两个线程T1和T2为例,但实际上也适用于更多个线程。

首先,假定两个线程分属两个不同的进程,并且互相之间也没有共享内存、不访问共同的外设,那么它们就是“井水不犯河水”,各做各的事情,跟系统是否SMP结构无关,所以天然就是“SMP安全”的。即使两个线程在执行相同的代码,也是如此(只要在运行过程中不会修改代码段就行)

总之,一般而言,只有可以被多个线程共享的变量才有冲突,因而才需要“互斥”,即在一个线程访问共享变量时不允许别的线程前来干扰。

如果两个线程属于同一个进程,那么由于共享同一个用户空间,互相之间就可能有冲突了。引起这种冲突的原因可能有:

全局变量(包括static局部量),或者更确切地说是“共享量”、队列、共享内存

下面举例说明这个问题。

假定线程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却是另一个线程的数据结构。所以T1T2完全有可能从不同的路径同时进入create_msg_queue()这个函数,而赋值语句“thread->queue = queue”中的指针thread则指向同一个数据结构。于是,T1T2在这个函数中各自分配了一个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的值是否有可能被覆盖呢?不会。这是因为T1T2的局部量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;

            ......

}

这是单链的队列操作。如果T1T2同时执行到这里,就可能会发生这样的情况:

l    参数Directory指向同一个目录,所以两个线程的HeadDirectoryEntry相等。

l    但是两个线程各自分配了NewDirectoryEntry

l    两个线程也各有自己的Object

l    时间上T1T2略为领先。

l    于是T1NewDirectoryEntry就丢失了,属于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有条指令XADDIntel的手册中关于这条指令的定义是这样:

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,则有可能在同一个物理时间点上发生冲突,所以更难以保证其原子性。

那么,哪一些操作应该是原子的,又怎样才能保证这些操作的原子性呢?

先看哪一些操作应该是原子的:

对同一个变量的修改(读出-改变-回写)。

队列操作。

某些赋值操作。

其它需要放在临界区中完成的操作序列。

再考虑怎样保证原子性的问题。

先看在单CPU的多线程系统怎么保证操作的原子性:

在单CPU的多线程系统中,要保证用户空间的操作的原子性,就只有两个办法:

放在同一条指令中完成。

放在临界区中完成。

对于用户空间的程序,要切记:任何两条指令之间都可以发生中断和线程切换。因此,只要一个原子操作的粒度超过单条指令,就一定要放在临界区中。另一方面,只要把操作放在了临界区中,一般而言就是安全的。

而如果是在内核中,那么情况有所不同:

在内核中,并非任何两条指令之间都可以发生中断,并可以在程序中关中断,形成一个“中断禁区”。中断禁区中既然不会发生中断,则自然也不会发生(被动的)调度,所以同时又是“调度禁区”。

即使发生中断,也不一定可以发生线程切换,具体取决于调度策略:

1. 如果是实时调度,那么应该认为任何两条指令之间都可以发生线程切换(实际上在中断服务程序里面是不会发生切换的)。

2.  如果是非实时调度,那么基本上不会发生线程切换。

除调度策略的影响外,在程序中还可以禁止调度,形成一个“调度禁区”。在调度禁区中仍可发生中断,但不会发生调度。

有些原子操作光是放在普通的临界区中还不够,还需要把中断关掉。例如,假定中断服务程序从网卡读包,并将包挂入接收队列;而应用程序通过系统调用从接收队列摘包,那就要用临界区+关中断的方案解决。

由此可见,不管是用户空间还是系统空间,临界区是个保证操作原子性的基本手段。除临界区外,在内核中也可以用调度禁区和中断禁区保证操作的原子性。其中调度禁区可以防止不同线程之间的冲突,而中断禁区可以防止(因中断引起的)同一线程的自相冲突。

至于临界区的实现,在单CPU和多CPU系统中又有不同的考虑。

在单CPU的系统中,临界区要防止的是不同线程针对共享变量的操作的穿插和混合,以实现有序的串行化。这决定了:

一时不能进入临界区的线程必须让出CPU(这是系统中唯一的CPU),使已经进入临界区的线程有机会完成其操作并退出临界区。换言之,在进不了临界区的时候必须睡眠等待。与此相应,每当有线程从临界区退出时必须唤醒可能正在睡眠等待的线程。

 

但是,在多CPU的系统中,则有所不同:

不存在必须让出CPU的问题,因为有多个CPU

要让出CPU就要用到睡眠/唤醒的机制,既有系统开销代价可能太大的问题,也有时间延迟的问题,具体要看(临界区中)原子操作的大小。

实际上原子操作一般都是不大的,让一时不能进入临界区的线程空转等待往往更好。如果采用空转等待,那就是空转锁、即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(),这一点从kernell32spec文件中可以看出:

@ 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);

            ......

}

    如果两个线程T1T2同时运行到这儿,几乎同时进入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;

            ......

}

这是单链的队列操作。如果T1T2同时执行到这里,就同样会因丢失数据结构

而造成内存泄露。

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;

由此可见,如果忽略DEBUGLOCKBREAK的需要,一个spinlock实际上就是一个无符号整数。

在内核中,需要定义一个spinlock时可以这样:

#define DEFINE_SPINLOCK(x)  spinlock_t  x = __SPIN_LOCK_UNLOCKED(x)

再看include/linux/spinlock.hspin_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()。其它就不多说了。


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值