存档在 2010年10月

指向一维数组的指针

2010年10月30日

今天在看到typedef int (*int_arry)[10];这条语句时,因为对这样的定义使用较少,就想着编写一个test.c来试试看。不过,当我编写完一个简单的测试程序时,却发现我对指向一维数组的指针的使用了解甚少。

起初,我的程序是这样:

#include  < stdio.h >
typedef int (*int_array)[10];
int main()
{

	int a[10]={1,2,3,4,5};
	int_array i=&a;

	printf("%d=%d\n",i[4],a[4]);
      return 0;
}

编译后,提示如下错误:

test.c: In function ‘main’:
test.c:10: warning: format ‘%d’ expects type ‘int’, but argument 2 has type ‘int *’

也就是说,i[4]是一个int*型的指针。为什么会出现这样的错误?既然i是一个指向有10个整形元素数组的指针。那么将i指向数组a,然后使用i[4]获取第四个元素有什么错?

那我们从另一个角度来分析,一般i[4]这样的形式都可以看成*(i+4)这样的形式。i+4是什么?对了!i是一个数组指针,那么i+4也就是一个数组指针。如果将i所指的数组看作一个二维表的第1行,那么i+4就是指向第5行的指针。也就是说它相对于i所指向位置的偏移量为4*sizeof(int)*10个字节。因此*(i+4)仍然是一个指针,只不过它指向第5行的首个元素。

看来我们找到问题所在,i[4]并不是一个整形元素,而是一个指向整形元素的指针。上面程序中,我原本的意思是通过i来打印数组a中第四个元素。那么此刻我们应该这么修改:

printf("%d=%d\n",i[0][4],a[4]);

或者下面任意一句:

printf("%d=%d\n",(*(i+0))[4],a[4]);
printf("%d=%d\n",*(i[0]+4),a[4]);
printf("%d=%d\n",*(*(i+0)+4),a[4]);

你会发现,如果p是一个指向指针的指针,那么总能够通过两次的*、两次的[]或者一次*和一次[]取得这个指针最终指向的数据,因为说到底[]总能够化成*的形式。理解了这些,上面的语句对你也就不成问题了。got it?

支持阻塞操作的字符设备驱动

2010年10月27日

前文中,我们已经知道了如何编写一个简单的字符设备驱动。本文将在此基础上为这个字符设备驱动增加阻塞功能。不过在此之前,我们会先做一些准备工作。

阻塞和非阻塞I/O

阻塞和非阻塞I/O是设备访问内核的两种不同的模式。进程以阻塞方式访问设备并对其进行操作时,如果不能及时获得I/O资源则会被挂起,直到满足可操作的条件后才进行相应的操作。这个被挂起的进程会进入睡眠状态,并被移至某个等待队列;当条件满足时候,会被移出这个等待队列。这里所说的等待队列以及相关操作在上文已说明,在此不再赘述。非阻塞方式是指进程在不能进行设备操作时并不被挂起,它要么放弃操作,要么不停进行查询,直至可以进行相关的设备操作为止。

我们现在已经知道,用户空间中的应用程序通过read()和write()等统一的系统调用来访问设备文件。而这些系统调用函数最终则会调用设备驱动中的XXX_read()和XXX_write()函数。因此,如果我们在设备驱动中实现了阻塞功能(具体会落实到某个操作函数),当应用程序进程不能及时获得设备资源时就会将该进程阻塞到资源可访问为止。那么XXX_read()和XXX_write()等函数也就不会立即返回,read()和write()等系统调用也就不会立即返回。整个阻塞-唤醒的过程用户是无法感知到的。从用户的角度来看,他们会认为直接就可以对此设备进行操作。

相反,如果设备驱动中的操作函数是非阻塞的,那么当设备资源不可用时,设备驱动中的XXX_read()和XXX_write()等函数会立即返回,那么read()和write()等系统调用也会立即返回。从用户角度来看,此时访问设备文件就出错了。

支持阻塞操作的字符设备驱动

接下来要分析的这个字符设备驱动同样使用一个全局的字符串数组globalmem来存储字符数据。XXX_read()负责将内核空间的数据(此处即为globalmem中的字符串)拷贝到用户空间,实现用户空间对设备文件的读操作;XXX_write()负责将用户空间的数据拷贝到内核空间,实现用户空间对该设备文件的写操作。另外,为了更好的演示本文所述的阻塞操作,我们对这个字符串数组globalmem进行这样的限制:当它为空时,读进程不能进行读操作;当它为满的时候,写进程不能进行写操作。当读了count字节的数据后,还要将globalmem中这些被读的数据移出这个全局数组。

如果你理解了前面那个最基本的字符设备驱动的话,除了上述的不同外,基本上没有什么地方你看不懂的。这个举例的完整代码在这里

static char globalmem[BUF_NUM];
static wait_queue_head_t rdwait;
static wait_queue_head_t wrwait;
static struct semaphore mutex;

static int len;
ssize_t myblock_read(struct file*,char*,size_t count,loff_t*);
ssize_t myblock_write(struct file*,char*,size_t count,loff_t*);

struct file_operations fops=
{
	.read=myblock_read,
	.write=myblock_write,
};

static int __init myblock_init(void)
{
	int ret;

	printk("myblock module is working..\n");

	ret=register_chrdev(MAJOR_NUM,"edsionte_block_cdev",&fops);
	if(ret<0)
	{
		printk("register failed..\n");
		return 0;
	}
	else
	{
		printk("register success..\n");
	}
	init_MUTEX(&mutex);
	init_waitqueue_head(&rdwait);
	init_waitqueue_head(&wrwait);

	return 0;
}

在内核模块加载函数中,先申请字符设备号;再初始化互斥信号量mutex;最后分别初始化了读等待队列头和写等待队列头。另外定义了一个全局变量len来记录当前globalmem中实际的字节数,而BUF_NUM则是最大长度。

在读函数中,我们先创建一个代表当前进程的等待队列结点wait,并把它加入到读等待队列当中。但这并不意味着当前进程就已经完全睡眠了,还需要调度函数的调度。我们前面已经说过,当共享数据区的数据长度为0时,就应该阻塞该进程。因此,在循环中,首先将当前进程的状态设置TASK_INTERRUPTIBLE。然后利用schedule函数进行重新调度,此时,读进程才会真正的睡眠,直至被写进程唤醒。在睡眠途中,如果用户给读进程发送了信号,那么也会唤醒睡眠的进程。

当共享数据区有数据时,会将count字节的数据拷贝到用户空间,并且唤醒正在睡眠的写进程。当上述工作完成后,会将当前进程从读等待队列中移除,并且将当前进程的状态设置为TASK_RUNNING。

关于从全局缓冲区移出已读数据,这里要特别说明一下。这里利用了memcpy函数将以(globalmem+count)开始的(len-count)字节的数据移动到缓冲区最开始的地方。

另外,在上述操作过程中,还加入了互斥信号量防止多个进程同时访问共享数据len和globalmem。

ssize_t myblock_read(struct file*fp,char*buf,size_t count,loff_t*offp)
{
	int ret;
	DECLARE_WAITQUEUE(wait,current);

	down(&mutex);
	add_wait_queue(&rdwait,&wait);

	while(len==0)
	{
		__set_current_state(TASK_INTERRUPTIBLE);
		up(&mutex);
		schedule();
		if(signal_pending(current))
		{
			ret=-1;
			goto signal_out;
		}

		down(&mutex);
	}

	if(count>len)
	{
		count=len;
	}

	if(copy_to_user(buf,globalmem,count)==0)
	{
		memcpy(globalmem,globalmem+count,len-count);
		len-=count;
		printk("read %d bytes\n",count);
		wake_up_interruptible(&wrwait);
		ret=count;
	}
	else
	{
		ret=-1;
		goto copy_err_out;
	}

copy_err_out:up(&mutex);
signal_out:remove_wait_queue(&rdwait,&wait);

	set_current_state(TASK_RUNNING);
	return ret;
}

在写函数中,如果检测到globalmem当前的长度是BUF_NUM,则阻塞当前的进程;否则,从用户空间将数据拷贝到内核空间。写函数的控制流程大致与读函数相同,只不过对应的等待队列是写等待队列。

ssize_t myblock_write(struct file*fp,char*buf,size_t count,loff_t*offp)
{
	int ret;
	DECLARE_WAITQUEUE(wait,current);

	down(&mutex);
	add_wait_queue(&wrwait,&wait);

	while(len==BUF_NUM)
	{
		__set_current_state(TASK_INTERRUPTIBLE);
		up(&mutex);
		schedule();
		if(signal_pending(current))
		{
			ret=-1;
			goto signal_out;
		}

         	down(&mutex);
	}
	if(count>(BUF_NUM-len))
	{
		count=BUF_NUM-len;
	}

	if(copy_from_user(globalmem+len,buf,count)==0)
	{
		len=len+count;
		printk("written %d bytes\n",count);
		wake_up_interruptible(&rdwait);
		ret=count;
	}
	else
	{
		ret=-1;
		goto COPY_ERR_OUT;
	}

signal_out:up(&mutex);
COPY_ERR_OUT:remove_wait_queue(&wrwait,&wait);
	set_current_state(TASK_RUNNING);

	return ret;
}

上述就是支持阻塞模式的字符设备驱动。关于上述程序更多的解释如下:

1.两种睡眠。当读进程读数据时,如果发现写进程正在访问临界区,那么它会因为不能获得互斥信号量而阻塞;而当读进程获得信号量后,如果当前globalfifo的数据数为0,则会阻塞。这种阻塞是由我们在设备驱动中实现的。

2.两种唤醒。当写进程离开临界区并释放信号量时,读进程会因信号量被释放而唤醒;当写进程往globalfifo中写入了数据时,读进程会被写进程中的唤醒函数所唤醒。特别的,如果读进程是以轻度睡眠方式睡眠的,那么用户可以通过发送信号而唤醒睡眠的读进程。

3.唤醒后如何执行。无论因哪种方式而睡眠,当读进程被唤醒后,均顺序执行接下来的代码。

4.down操作和add_wait_queue操作交换。在原程序中,读进程先获取信号量,再将读进程对应的等待队列项添加到读等待队列中。如果交换,当读进程的等待队列项加入到等待队列后,它可能又会因未获得信号量而阻塞。

5.up操作和remove_wait_queue操作交换。这两个操作分别对应标号out和out2。如果读进程从内核空间向用户空间拷贝数据失败时,就会跳转到out。因为读进程是在获得信号量后才拷贝数据的,因此必须先释放信号量,再将读进程对应的等待队列项移出读等待队列。而当读进程因信号而被唤醒时,则直接跳转到out2。此时读进程并没有获得信号量,因此只需要移出队列操作即可。如果交换上述两个操作,读进程移出等待队列时还未释放互斥信号量,那么写进程就不能写。而当读进程因信号而唤醒时,读进程并没有获得信号量,却还要释放信号量。

通过下述方法,你就可以体验到以阻塞方式访问设备文件。

1.make编译文件,并插入到内核;
2.创建设备文件结点:sudo mknod /dev/blockcdev c major_num 0;
3.修改设备文件权限:sudo chmod 777 /dev/blockcdev;
4.终端输入:cat /dev/blockcdev&;即从字符设备文件中读数据,并让这个读进程在后台执行,可通过ps命令查看到这个进程;
5.中断继续输入:echo ‘I like eating..’ > /dev/blockcdev;即向字符设备文件中写入数据;

通过上述的步骤,可以看到,每当利用echo命令写入数据时,后台运行的读进程就会读出数据,否则读进程一直阻塞。此外,如果你愿意的话,还可以分别编写一个读写进程的程序进行测试。

等待队列源码分析

2010年10月26日

正如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日

在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日

我们知道,一个进程是由进程控制块(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,编译,插入内核,再查看内核日志文件吧!

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