等待队列源码分析

2010年10月26日 由 edsionte 2 条评论 »

正如list_head结构那样,等待队列(wait queue)作为linux内核中的基础数据结构,与进程调度紧密结合在一起;在驱动程序中,常常使用等待队列来实现进程的阻塞和进程的唤醒。因此,我们很有必要对它的内部实现进行分析。

0.数据结构

一般我们的链式线性表都会有一个头结点,以使我们迅速找到这个线性链表的“领导”。在等待队列中,同样有队列头,只不过等待队列头和普通的等待队列结点定义有所不同。

  
 //yoursource/include/linux/wait.h
 50struct __wait_queue_head {
  51        spinlock_t lock;
  52        struct list_head task_list;
  53};
  54typedef struct __wait_queue_head wait_queue_head_t;

可以看到,等待队列头结构中封装了list_head结构。这么做是可以想象到的,因为队列和栈本质上还是双联表。当我们适当限制双联表的某些操作时,就可以实现这样的功能。另外,等待队列头结构中还有一个自旋锁结构的变量lock,它起到了对等待队列进行互斥操作的作用。

在等待队列结构中,除了使用list_head结构外,还有以下几个字段:

  28typedef struct __wait_queue wait_queue_t;
  32struct __wait_queue {
  33        unsigned int flags;
  34#define WQ_FLAG_EXCLUSIVE       0x01
  35        void *private;
  36        wait_queue_func_t func;
  37        struct list_head task_list;
  38};

flag:指明该等待的进程是互斥还是非互斥,为0时非互斥,为1时互斥;
WQ_FLAG_EXCLUSIVE :从变量可以看出,此宏代表进程是互斥的;
private:void型指针变量功能强大,你可以赋给它你需要的结构体指针。一般赋值为task_struct类型的指针,也就是说指向一个进程;
func:函数指针,指向该等待队列项的唤醒函数;

我们可以通过下述图来详细了解等待队列头和等待队列的结构关系:

1.定义及初始化

内核中使用init_waitqueue_head宏来初始化等待队列头。

//yourLinuxSourceDir/include/linux/wait.h
  82#define init_waitqueue_head(q)                          \
  83        do {                                            \
  84                static struct lock_class_key __key;     \
  85                                                        \
  86                __init_waitqueue_head((q), &__key);     \
  87        } while (0)

我们可以看到这个宏使用了do-while型循环语句,里面包含两条语句。首先定义了一个变量__key,然后再调用init_waitqueue_head函数。

事实上,这个do-while循环语句只会执行一次。那么,为什么要选择使用这个循环语句?在定义宏的时候将上述语句嵌套在一个大括号里也可以啊!可能我们如下那样使用一个宏:

 10         if(conditon)
 11                 init_waitqueue_head(q);
 12         else
 13                 do_somthing_else();

如果我们去除do-while,那么替换后会编译错误。因为宏末尾的分号使得else语句成为一个单独的句子。你也许会说,那我这样使用:init_waitqueue_head(q)就可以避免这个错误了。这样是可以,但是对于那些初学者来说,很难避免他们要加上一个;。并且,在茫茫代码中,孤零零的出现一个没有;的语句,未免显得有些奇怪。

言归正传,在init_waitqueue_head宏中调用的__init_waitqueue_head函数定义如下:

 13void __init_waitqueue_head(wait_queue_head_t *q, struct lock_class_key *key)
  14{
  15        spin_lock_init(&q->lock);
  16        lockdep_set_class(&q->lock, key);
  17        INIT_LIST_HEAD(&q->task_list);
  18}

在这个函数中,首先利用自旋锁初始化函数初始化这个自旋锁;在上述等待队列头的定义中,我们可以看到task_list字段是一个list_head结构类型,因此我们使用INIT_LIST_HEAD对这个链表进行初始化。这些过程都是我们所熟悉的。

同时,我们也可以使用初始化宏DECLARE_WAIT_QUEUE_HEAD一步的进行定义和初始化。

  70#define __WAIT_QUEUE_HEAD_INITIALIZER(name) {                           \
  71        .lock           = __SPIN_LOCK_UNLOCKED(name.lock),              \
  72        .task_list      = { &(name).task_list, &(name).task_list } }
  73
  74#define DECLARE_WAIT_QUEUE_HEAD(name) \
  75        wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

上述代码对于有C基础的人理解起来并不困难。需要注意的是对task_list进行赋值后,这个结点的前后指针都会指向自己。

同样,对于等待队列,我们可以使用DECLARE_WAITQUEUE宏来定义并初始化一个等待队列项。

  30int default_wake_function(wait_queue_t *wait, unsigned mode, int flags, void *key);
  62#define __WAITQUEUE_INITIALIZER(name, tsk) {                            \
  63        .private        = tsk,                                          \
  64        .func           = default_wake_function,                        \
  65        .task_list      = { NULL, NULL } }
  66
  67#define DECLARE_WAITQUEUE(name, tsk)                                    \
  68        wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

name要定义的等待队列项的名称;tsk是task_struct类型的指针变量,它指向这个等待队列项所对应的进程。

3.添加/移除等待队列

add_wait_queue添加函数将等待队列wait添加到以q为等待队列头的那个等待队列链表中。

22void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
23{
24        unsigned long flags;
25
26        wait->flags &= ~WQ_FLAG_EXCLUSIVE;
27        spin_lock_irqsave(&q->lock, flags);
28        __add_wait_queue(q, wait);
29        spin_unlock_irqrestore(&q->lock, flags);
30}

我们可以看到flags的结果必然是0,也就是说这个函数是将非互斥进程添加到等待队列当中。而且在调用具体的添加函数时候,使用关中断并保护现场的自旋锁方式使得添加操作每次只被一个进程访问。

具体的添加过程是将当前进程所对应的等待队列结点wait添加到等待队列头结点q之后。具体来说,就是将new->task_list结点添加到以head->task_list为头指针的双链表当中。另外,通过add_wait_queue_exclusive函数可以将一个互斥进程添加到等待队列当中。从添加过程可以发现,add_wait_queue函数会将非互斥进程添加到等待队列的前部。

 122static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)
 123{
 124        list_add(&new->task_list, &head->task_list);
 125}

另外, add_wait_queue_exclusive添加函数则会将互斥进程添加到等待队列的末尾。

  33void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait)
  34{
  35        unsigned long flags;
  36
  37        wait->flags |= WQ_FLAG_EXCLUSIVE;
  38        spin_lock_irqsave(&q->lock, flags);
  39        __add_wait_queue_tail(q, wait);
  40        spin_unlock_irqrestore(&q->lock, flags);
  41}

remove_wait_queue函数用于将等待队列项wait从以q为等待队列头的等待队列中移除,源码如下。

  44void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
  45{
  46        unsigned long flags;
  47
  48        spin_lock_irqsave(&q->lock, flags);
  49        __remove_wait_queue(q, wait);
  50        spin_unlock_irqrestore(&q->lock, flags);
  51}
 150static inline void __remove_wait_queue(wait_queue_head_t *head,
 151                                                        wait_queue_t *old)
 152{
 153        list_del(&old->task_list);
 154}

有了上述的基础,那么移除函数就简单了许多。

4.在等待队列上睡眠

如何实现进程的阻塞?大致过程就是将当前进程的状态设置成睡眠状态,然后将这个进程加入到等待队列即可。在linux内核中有一组函数接口来实现这个功能。

4313void __sched interruptible_sleep_on(wait_queue_head_t *q)
4314{
4315        sleep_on_common(q, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
4316}
4317EXPORT_SYMBOL(interruptible_sleep_on);
4318
4319long __sched
4320interruptible_sleep_on_timeout(wait_queue_head_t *q, long timeout)
4321{
4322        return sleep_on_common(q, TASK_INTERRUPTIBLE, timeout);
4323}
4324EXPORT_SYMBOL(interruptible_sleep_on_timeout);
4325
4326void __sched sleep_on(wait_queue_head_t *q)
4327{
4328        sleep_on_common(q, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
4329}
4330EXPORT_SYMBOL(sleep_on);
4331
4332long __sched sleep_on_timeout(wait_queue_head_t *q, long timeout)
4333{
4334        return sleep_on_common(q, TASK_UNINTERRUPTIBLE, timeout);
4335}
4336EXPORT_SYMBOL(sleep_on_timeout);

通过上述源码,你可以发现这些函数在内部都调用了sleep_on_common函数,通过传递不同的值来实现不同的功能。而这个通用函数的三个参数分别关注的是:进程要加入到那个等待队列?进程是那种睡眠状态(TASK_UNINTERRUPTIBLE还是TASK_INTERRUPTIBLE)?进程睡眠的时间?

4292static long __sched
4293sleep_on_common(wait_queue_head_t *q, int state, long timeout)
4294{
4295        unsigned long flags;
4296        wait_queue_t wait;
4297
4298        init_waitqueue_entry(&wait, current);
4299
4300        __set_current_state(state);
4301
4302        spin_lock_irqsave(&q->lock, flags);
4303        __add_wait_queue(q, &wait);
4304        spin_unlock(&q->lock);
4305        timeout = schedule_timeout(timeout);
4306        spin_lock_irq(&q->lock);
4307        __remove_wait_queue(q, &wait);
4308        spin_unlock_irqrestore(&q->lock, flags);
4309
4310        return timeout;
4311}

在此函数中,首先定义了一个等待队列项结点,通过 init_waitqueue_entry函数对其进行初始化。可以从下述初始化源码中看到,此时该等待队列项指向当前当前进程。而唤醒函数指针func则指向内核自定义的一个默认唤醒函数default_wake_function。

  98static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)
  99{
 100        q->flags = 0;
 101        q->private = p;
 102        q->func = default_wake_function;
 103}

初始化完毕后,通过__set_current_state函数将当前进程的状态设置成state。接着,在自旋锁的保护下,将当前进程对应的等待队列结点插入到等待队列链表当中。更重要的是,在schedule_timeout函数中不仅要设置进程的睡眠时间(以jiffies为单位的),还要使用schedule函数进行重新调度。一旦使用了schedule函数后,也就意味这当前这个进程真正的睡眠了,那么接下来的代码会在它唤醒后执行。当该进程被唤醒后(资源可用时),会从等待队列中将自己对应的那个等待队列结点wait移除。

上述过程都是在自旋锁保护下进行的,并且在整个执行过程中不可被其他中断所打断。现在再回过头去看一开始的那四个睡眠函数接口,你就明白了它们各自的不同之处了。

5.唤醒函数

唤醒函数会唤醒以x为头结点的等待队列中的等待队列项所对应的进程。与睡眠函数类似,内核中也有一组函数可以对阻塞的进程进行唤醒。

 170#define wake_up(x)                      __wake_up(x, TASK_NORMAL, 1, NULL)
 171#define wake_up_nr(x, nr)               __wake_up(x, TASK_NORMAL, nr, NULL)
 172#define wake_up_all(x)                  __wake_up(x, TASK_NORMAL, 0, NULL)

 175#define wake_up_interruptible(x)        __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
 176#define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
 177#define wake_up_interruptible_all(x)    __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)

通过上述代码,我们可以发现这些唤醒函数均调用了__wake_up函数。__wake_up函数的四个参数分别指:头结点指针、唤醒进程的类型、唤醒进程的数量和一个附加的void型指针变量。

3999void __wake_up(wait_queue_head_t *q, unsigned int mode,
4000                        int nr_exclusive, void *key)
4001{
4002        unsigned long flags;
4003
4004        spin_lock_irqsave(&q->lock, flags);
4005        __wake_up_common(q, mode, nr_exclusive, 0, key);
4006        spin_unlock_irqrestore(&q->lock, flags);
4007}

在__wake_up函数又通过传递不同的参数调用__wake_up_common函数来实现不同的唤醒功能。

3975static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
3976                        int nr_exclusive, int wake_flags, void *key)
3977{
3978        wait_queue_t *curr, *next;
3979
3980        list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
3981                unsigned flags = curr->flags;
3982
3983                if (curr->func(curr, mode, wake_flags, key) &&
3984                                (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
3985                        break;
3986        }
3987}

list_for_each_entry_safe函数将遍历整个等待队列中的链表,通过每次的逻辑判断来唤醒相应的进程。这个if语句初看起来有点麻烦,不过我们一点一点的将它拆解。

curr->func(curr, mode, sync, key):即执行默认的唤醒函数,将指定的进程curr以mode方式唤醒。成功唤醒返回1;否则,返回0;
(flags & WQ_FLAG_EXCLUSIVE):判断当前进程是否以互斥形式唤醒。是互斥形式则返回1;否则返回0;
!–nr_exclusive:nr_exclusive为需要唤醒的互斥进程的数量。

这三个部分是通过逻辑与连接起来的。根据逻辑与的运算规则,只有当第一部分为真时才会判断第二部分的值,依次再判断第三部分的值。

通过上述的等待队列的添加过程我们知道,等待队列中前面均是非互斥进程,后面才是互斥进程。因此,唤醒函数总先唤醒全部的非互斥进程。因为当__wake_up_commom函数每一次去判断if语时,总会“不自觉”去执行默认的唤醒函数(除非唤醒失败,那么会退出遍历宏);当全部的非互斥进程被唤醒后,第二个判断条件也就成立了。因此__wake_up_commom函数会依次唤醒nr_exclusive个互斥进程;当–nr_exclusive为0时(!–nr_exclusive也就为真),整个遍历过程也恰好结束,而此时if语句的三个判断条件才刚好满足(这段代码太强大了!!!)。

6.有条件的睡眠

与睡眠函数不同,条件睡眠是指当某些条件发生时,这个进程才会加入到等待队列当中。关于条件睡眠有下列的宏:

wait_event(wq, condition)
wait_event_timeout(wq, condition, timeout)
wait_event_interruptible(wq, condition)
wait_event_interruptible_timeout(wq, condition, timeout)

关于条件睡眠,虽然函数实现与睡眠函数不同,但是基本思想是相似的,各位可以查找相应的源码进行分析。Try!

生产者-消费者模型(使用内核线程实现)

2010年10月25日 由 edsionte 1条评论 »

在IPC系列文章当中,利用信号量集实现的是用户态下生产者-消费者模型的模型。本文以内核模块的方式,通过创建内核线程来为大家演示内核态下的生产者消费者模型。本模型属于np-nc-nb,即多个生产者多个消费着多个缓冲区。

在加载函数中,完成一些初始化的工作,并分别创建了5个生产者线程和消费者线程。kernel_thread函数的第一个参数是所所创建线程要做的动作;通过第二个参数传递i变量。

static int __init np_nc_init(void)
{
	int i;

	printk("np_nc module is working..\n");
	in=out=0;
	cflag=0;
	init_MUTEX(&mutex);
	sema_init(&s1,BUF_NUM);
	sema_init(&s2,0);

	for(i=0;i< N;i++)
	{
		index[i]=i+1;
		kernel_thread(productor_thread,&(index[i]),CLONE_KERNEL);
		kernel_thread(consumer_thread,&(index[i]),CLONE_KERNEL);
	}

	return 0;
}

在生产者函数中,PNUM是每个生产者要生产货物的数量。语句buf[in]=i*100+(PNUM-p_num+1)即是产生货物的过程。

int productor_thread(void *p)
{
	int i=*(int *)p;
	int p_num=PNUM;

	while(p_num)
	{
		if((s1.count)<=0)
		{
			printk("[producer %d,%d]:I will be waiting for producting..\n",i,s1.count);
		}
		down(&s1);
	        down(&mutex);
          	buf[in]=i*100+(PNUM-p_num+1);
          	printk("[producer %d,%d]:I producted a goods \"%d\" to buf[%d]..\n",i,s1.count,buf[in],in);
            	in=(in+1)%BUF_NUM;
         	up(&mutex);
          	up(&s2);
		p_num--;
	}
	return 0;
}

在消费者函数中,通过一个全局变量CNUM来控制消费者总共的消费次数。

int consumer_thread(void *p)
{
	int i=*(int *)p;
	int goods;

	while(cflag!=CNUM)
	{
		if((s2.count)<=0)
		{
			printk("[consumer %d,%d]:I will be waiting for goods..\n",i,s2.count);
		}
	        down(&s2);
            	down(&mutex);
          	goods=buf[out];
           	printk("[consumer %d,%d]:I consumed a goods \"%d\" from buf[%d]..\n",i,s2.count,goods,(out%BUF_NUM));
           	out=(out+1)%BUF_NUM;
            	up(&mutex);
            	up(&s1);
		cflag++;
	}
	return 0;
}

遍历进程链表

2010年10月25日 由 edsionte 3 条评论 »

我们知道,一个进程是由进程控制块(PCB),代码段和数据段组成的;并且,OS通常是通过PCB来感知一个进程的存在。其实PCB就是操作系统对每个进程的代码描述。linux内核中使用task_struct结构来描述一个PCB(具体可以在linux/kernel/sched.c查看源码);多个进程则常常使用双链表等来进行组织。比如可运行状态的进程组成可运行队列,等待状态的进程组成等待队列等。

本文将使用前文中所分析的list_head结构来遍历内核中的进程链表。task_struct结构中使用多个字段来详细的描述一个进程。本文中所属的遍历函数只打印一些常用的信息,即进程名称,进程的pid。

我们利用list.h中的下述遍历宏对整个进程链表进行遍历:

420#define list_for_each_entry(pos, head, member)                          \
421        for (pos = list_entry((head)->next, typeof(*pos), member);      \
422             prefetch(pos->member.next), &pos->member != (head);        \
423             pos = list_entry(pos->member.next, typeof(*pos), member))

先简单了解一下这个宏的使用方法。由于list_head结构中没有数据字段,所以它经常被嵌套在其他的结构体当中。pos指向包含list_struct结构的结构体;head为list_head类型的指针,我们可以使用链表中任意一个结点的指针;member即为list_head类型的变量名。

对应到我们下面的程序,pos为指向task_struct类型的指针,我们通过遍历宏即得到每个进程对应的task_struct类型的指针;我们将current_head赋予遍历宏中的第二个参数,current是一个宏,即为系统内正在运行的进程;由于list_struct结构在task_struct结构中的变量明为tasks,因此我们将tasks传递给便利宏的第三个参数。

#include< linux/init.h >
#include< linux/module.h >
#include< linux/sched.h >
#include< linux/sem.h >
#include< linux/list.h >

static int __init  traverse_init(void)
{
      struct task_struct *pos;
      struct list_head *current_head;
      int count=0;

      printk("Traversal module is working..\n");
      current_head=&(current->tasks);
      list_for_each_entry(pos,current_head,tasks)
      {
             count++;
             printk("[process %d]: %s\'s pid is %d\n",count,pos->comm,pos->pid);
      }
      printk(KERN_ALERT"The number of process is:%d\n",count);
      return 0;
}

了解了便利宏的使用方法,那么理解上述代码就简单的多了。关于便利宏的代码详解可参考这里的文章。

加载函数要做的工作就是这些,那么卸载函数的功能就相对于简单多了。

static void __exit traverse_exit(void)
{
  printk("hello world exit\n");
}

OK,编译,插入内核,再查看内核日志文件吧!

内核同步方法-锁机制

2010年10月21日 由 edsionte 1条评论 »

本文将为你介绍内核同步算法中的自旋锁和信号量。在这之前,先了解一些概念。

执行线程:thread of execution,指任何正在执行的代码实例,可能是一个正在内核线程,一个中断处理程序等。有时候会将执行线程简称为线程。

临界区:critical region,即访问和操作共享数据的代码段。

多个执行线程并发访问同一资源通常是不安全的,通常使用自旋锁和信号量来实现对临界区互斥的访问。

自旋锁

自旋锁(spin lock)是一个对临界资源进行互斥访问的典型手段。自旋锁至多只能被一个执行线程持有。当一个执行线程想要获得一个已被使用的自旋锁时,该线程就会一直进行忙等待直至该锁被释放,就如同“自旋”所表达的意思那样:在原地打转。

我们也可以这么理解自旋锁:它如同一把门锁,而临界区就如同门后面的房间。当一个线程A进入房间后,它会关闭房门,使得其他线程不得进入。此时如果其他某个进程B需要进入房间,那么只能在门外“打转”。当A进程打开们后,进程B才能进入房间。

自旋锁的使用

1.定义初始化自旋锁

使用下面的语句就可以先定一个自旋锁变量,再对其进行初始化:

spinlock_t lock;
spin_lock_init(&lock);

也可以这样初始化一个自旋锁:

spinlock_t lock=SPIN_LOCK_UNLOCKED;

2.获得自旋锁

void spin_lock(spinlock_t*);
int spin_trylock(spinlock_t*);

使用spin_lock(&lock)这样的语句来获得自旋锁。如果一个线程可以获得自旋锁,它将马上返回;否则,它将自旋至自旋锁的保持者释放锁。

另外可以使用spin_trylock(&lock)来试图获得自旋锁。如果一个线程可以立即获得自旋锁,则返回真;否则,返回假,此时它并不自旋。

3.释放自旋锁

void spin_unlock(spinlock_t*);

使用spin_unlock(&lock)来释放一个已持有的自旋锁。注意这个函数必须和spin_lock或spin_trylock函数配套使用。

关于自旋锁的说明

1.保护数据。我们使用锁的目的是保护临界区的数据而不是临界区的代码。
2.在使用自旋锁时候,必须关闭本地中断,否则可能出现双重请求的死锁。比如,中断处理程序打断正持有自旋锁的内核代码,如果这个中断处理程序又需要这个自旋锁,那么这个中断处理程序就会自旋等待自旋锁被释放;但是这个锁的持有者此刻被中断打断,因此不可能在中断完毕之前释放锁。此时就出现了死锁。
3.自旋锁是忙等锁。正如同前面所说的那样,当其他线程持有自旋锁时,如果另有线程想获得该锁,那么就只能循环的等待。这样忙的功能对CPU来说是极大的浪费。因此只有当由自旋锁保护的临界区执行时间很短时,使用自旋锁才比较合理。
4.自旋锁不能递归使用。这一点也很好理解,如果当前持有锁的线程又需要再持有该锁,那么它必须自旋,等待锁被释放;但是这个锁本身又被它自己持有,因此这个线程永远无法继续向前推进。

信号量

信号量(semaphore)是保护临界区的一种常用方法。它的功能与自旋锁相同,只能在得到信号量的进程才能执行临界区的代码。但是和自旋锁不同的是,一个进程不能获得信号量时,它会进入睡眠状态而不是自旋。

利用上述自旋锁的门和锁的例子,我们可以这样解释信号量。我们将信号量比作钥匙。进程A想要进入房间,就必要持有一把钥匙。当钥匙被使用完之后,如果又有进程B想进房间,那么这个进程在等待其他进程从房间出来给它钥匙的同时会打盹。当房间里的某个进程出来时会摇醒这个睡觉的进程B。

信号量的使用

0.数据结构

  16struct semaphore {
  17        spinlock_t              lock;
  18        unsigned int            count;
  19        struct list_head        wait_list;
  20};

count:初始化信号量时所设置的信号量值。
wait_list:等待队列,该队列中即为等待信号量的进程。
lock:自旋锁变量

1.定义初始化信号量

使用下面的代码可以定义并初始化信号量sem:

struct semaphore sem;
sem_init(&sem,val);

其中val即为信号量的初始值。

如果我们想使用互斥信号量,则使用下面的函数:

init_MUTEX(&sem);

这个函数会将sem的值初始为1,即等同于sem_init(&sem,1);

如果想将互斥信号量的初值设为0,则可以直接使用下面的函数:

init_MUTEX_LOCKED(&sem);

除上面的方法,可以使用下面的两个宏定义并初始化信号量:

DECLARE_MUTEX(name);

DECLARE_MUTEX_LOCKED(name);

其中name为变量名。

2.获得信号量

down(&sem);

进程使用该函数时,如果信号量值此时为0,则该进车会进入睡眠状态,因此该函数不能用于中断上下文中。

down_interruptibale(&sem);

该函数与down函数功能类似,只不过使用down而睡眠的进程不能被信号所打断,而使用down_interruptibale的函数则可以被信号打断。

如果想要在中断上下文使用信号量,则可以使用下面的函数:

dwon_try(&sem);

使用该函数时,如果进程可以获得信号量,则返回0;否则返回非0值,不会导致睡眠。

3.释放信号量

up(&sem);

该函数会释放信号量,如果此时等待队列中有进程,则唤醒一个。

信号量的同步

信号量除了使进程互斥的访问临界区外,还可以用于进程间的同步。比如,当B进程执行完代码区C后,A进程才会执行代码段D,两者之间有一定的执行顺序。

内核同步方法-原子操作

2010年10月20日 由 edsionte 没有评论 »

Latest Update:2010/10/23

原子操作

原子操作用于执行轻量级、仅执行一次的操作,比如修改计数器,某些条件下的增加值或设置位等。原子操作指的是指令以原子的方式执行。之所以用原子来修饰这种操作方式是因为它们均不可再分。也就是说,原子操作要么一次性执行完毕,要么就不执行,这些操作的执行过程是不可被打断的。原子操作的具体实现取决于体系架构,以下代码如无特别声明,均来自linux/arch/x86/include/asm/atomic.h中

内核中提供了两组原子操作的接口:原子整形操作和原子位操作。前者是一组对整形数据的操作,后者则是针对单独的位操作。通过上面的叙述我们可以知道,这写操作接口完成的动作都是原子的。

原子整形操作

0.数据结构

在linux/include/linux/types.h中有如下定义:

 190typedef struct {
 191        int counter;
 192} atomic_t;

原子类型内部只有一个整型的成员counter。

1.初始化/设置原子变量的值

  15#define ATOMIC_INIT(i)  { (i) }

  35static inline void atomic_set(atomic_t *v, int i)
  36{
  37        v->counter = i;
  38}

初始化宏的源码很明显的说明了如何初始化一个原子变量。我们在定义一个原子变量时可以这样的使用它:

  atomic_t v=ATOMIC_INIT(0);

atomic_set函数可以原子的设置一个变量的值。

2.获取原子变量的值

  23static inline int atomic_read(const atomic_t *v)
  24{
  25        return (*(volatile int *)&(v)->counter);
  26}

返回原子型变量v中的counter值。关键字volatile保证&(v->counter)的值固定不变,以确保这个函数每次读入的都是原始地址中的值。

3.原子变量的加与减

   47static inline void atomic_add(int i, atomic_t *v)
  48{
  49        asm volatile(LOCK_PREFIX "addl %1,%0"
  50                     : "+m" (v->counter)
  51                     : "ir" (i));
  52}

  61static inline void atomic_sub(int i, atomic_t *v)
  62{
  63        asm volatile(LOCK_PREFIX "subl %1,%0"
  64                     : "+m" (v->counter)
  65                     : "ir" (i));
  66}

加减操作中使用了内联汇编语句。

linux中的汇编语句都采用AT&T指令格式。带有C表达式的内联汇编格式为:__asm__ __volatile__(“InstructionList” :Output :Input :Clobber/Modify);其中asm(或__asm__)用来声明一个内联汇编语句;关键字volatile是可选的,选择此关键字意味着向编译器声明“不要动我所写的指令,我需要原封不动的保留每一条指令”,否则,编译器可能会对指令进行优化。InstructionList即我们所加的汇编语句。指令后面的3个部分(Output, Input,Clobber/Modify)是可选的,分别指输出操作数,输入操作数,修改位。

下面我们针对上述加减操作对上述内联汇编语句做简单的解释。

在加函数中,对变量加操作的原子型表现在使用单条的汇编语句完成。指令中的%1,%0分别代表输入操作数和输出操作数,它们用来标示变量。内联汇编语句中的操作数从0开始进行编号,并且优先为输出操作数编号,然后再依次为输入操作数编号。在指令后的输出部分,“+m”中的+表示输出变量是可读可写的,m表示输出操作数存储于内存单元中,而不是在寄存器中。在输入部分,“ir”中的i表示输入操作数是一个直接操作数,r表示存储与寄存器中。输入部分和输出部分中的括号内的C语言格式的变量分别对应汇编语句中的输入操作数和输出操作数。

汇编语句addl %1,%0就是将i加至v->counter上,这个加的过程是原子的。

了解了加操作,那么减操作也就不难理解了,这里不再赘述。

4.原子变量的自加自减

  93static inline void atomic_inc(atomic_t *v)
  94{
  95        asm volatile(LOCK_PREFIX "incl %0"
  96                     : "+m" (v->counter));
  97}

 105static inline void atomic_dec(atomic_t *v)
 106{
 107        asm volatile(LOCK_PREFIX "decl %0"
 108                     : "+m" (v->counter));
 109}

从内联的汇编语句中可以看到,自加自减均采用单条汇编指令,直接在输出数%0上加1或减1。这里的输出数%0的值即是v->counter变量的值。

5.操作并测试

atomic_sub_and_test函数实现的功能是原子的从v减去i,如果结果等于0,则返回1;否则返回0。

 77static inline int atomic_sub_and_test(int i, atomic_t *v)
  78{
  79        unsigned char c;
  80
  81        asm volatile(LOCK_PREFIX "subl %2,%0; sete %1"
  82                     : "+m" (v->counter), "=qm" (c)
  83                     : "ir" (i) : "memory");
  84        return c;
  85}

通过上述源码可以看到,第一条指令实现减操作;接着根据ZF的值设置c变量的值。sete命令的作用是,如果ZF=1,也就是说减的结果为0,将设置c变量为1;否则c的值为0。执行完毕后返回相应的c值。

 119static inline int atomic_dec_and_test(atomic_t *v)
 120{
 121        unsigned char c;
 122
 123        asm volatile(LOCK_PREFIX "decl %0; sete %1"
 124                     : "+m" (v->counter), "=qm" (c)
 125                     : : "memory");
 126        return c != 0;
 127}

 137static inline int atomic_inc_and_test(atomic_t *v)
 138{
 139        unsigned char c;
 140
 141        asm volatile(LOCK_PREFIX "incl %0; sete %1"
 142                     : "+m" (v->counter), "=qm" (c)
 143                     : : "memory");
 144        return c != 0;
 145}

上数两个函数的作用是自减1或自加1后,再测试v值是否为0,如果是0,则返回1;否则返回0。

也许你会认为上述的加/减并测试并不是原子的。我个人的认为是:第一条加/减操作的确是原子的,当此条语句执行完毕后,会产生相应的ZF值。如果此时在执行sete之前产生了中断,那么肯定会保存当前寄存器等值。因此,即便中间产生了中断,返回的也是中断前加/减后的结果。

6.操作并返回

 173static inline int atomic_add_return(int i, atomic_t *v)
 174{
 175        int __i;
 176#ifdef CONFIG_M386
 177        unsigned long flags;
 178        if (unlikely(boot_cpu_data.x86 <= 3))
 179                goto no_xadd;
 180#endif
 181        /* Modern 486+ processor */
 182        __i = i;
 183        asm volatile(LOCK_PREFIX "xaddl %0, %1"
 184                     : "+r" (i), "+m" (v->counter)
 185                     : : "memory");
 186        return i + __i;
 187
 188#ifdef CONFIG_M386
 189no_xadd: /* Legacy 386 processor */
 190        raw_local_irq_save(flags);
 191        __i = atomic_read(v);
 192        atomic_set(v, i + __i);
 193        raw_local_irq_restore(flags);
 194        return i + __i;
 195#endif
 196}

首先if判断语句判断当前的处理器是否为386或386以下,如果是则直接执行no_xadd处开始的代码;否则紧接着判断语句执行。此处的unlikely表示”经常不“。因为现在的处理器大都高于386,所以编译器一般都按照高于386来处理,以达到优化源代码的目的。搞清楚这段代码的逻辑关系,那么接下来的工作就不困难了。

如果CPU高于386,则它将执行xaddl命令,该命令的作用是先交换源和目的操作数,再相加。这里%0,%1分别代表i和v->counter。首先将i值用__i保存;操作数交换后,%0,%1的值依次为v->counter和i;相加并保存到%1;最后%0,%1的结果就是v->counter,i+v->counter,返回的结果i+__i的结果就是i+v->counter。这里涉及到i和v->counter的值时均指初值。

理解了上述过程,再看其他的操作返回函数:

 205static inline int atomic_sub_return(int i, atomic_t *v)
 206{
 207        return atomic_add_return(-i, v);
 208}

可以看到,atomic_sub_return函数是对atomic_add_return函数的封装。而下面两个函数则又是对上述两个函数的封装。

 210#define atomic_inc_return(v)  (atomic_add_return(1, v))
 211#define atomic_dec_return(v)  (atomic_sub_return(1, v))

上述就是对整形原子操作的分析。

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key