存档在 2010年8月

IPC under Linux-FIFO(2)

2010年8月23日

下面我们来详细说明有名管道的读写规则。

3.有名管道读取规则

(1)如果进程A事先写打开FIFO,但却不进行写操作(FIFO中为空),那么进程B对其进行读操作时候将会阻塞,或者直接返回-1(当设置了非阻塞标志时)。
我们继续以《LinuxC编程实战》一书中的例子(P252)为原型,对procwrite.c函数稍作改动即可,具体代码如下。

	printf("Now the writer will sleep 5s..\n");
	sleep(5);
	printf("Now the writer wake up and will write to FIFO..\n");
	write(fd,buf,strlen(buf)+1);
	printf("writer will leave!\n");
	close(fd);

我们可以发现,procwrite进程运行后,发生了阻塞,这一点可用上文的打开规则来解释。当在另一个终端运行procread进程后,此时的procwrite正常运行。由于我们让procwrite进程睡眠5s,所以当前管道缓冲区是空的,我们可以在另一终端发现procread进程发生了阻塞。但当procwrite睡眠结束,继续运行时,procread进程可正确读出管道内数据。

如果你理解了规则1,那么我们继续来了解两种读操作阻塞的情况。

(2)当FIFO中有数据时,但是正在被进程A所读取,如果B进程此时也欲读FIFO,B进程将阻塞;如果FIFO中无数据,那么读进程也将阻塞(规则1所述)。想要解除阻塞只要在管道内写入数据即可,不管写入多少数据,也不管读进程请求读多少数据。

我们继续以上述例子为原型,但是需要两个读进程。首先procwrite进程先进行第一次写,然后睡眠20s,接着进行第二次写操作,这是为了解除因读操作而阻塞的进程。

        //procwrite.c部分代码
	printf("Now the writer will write data to FIFO..\n");
	write(fd,buf,strlen(buf)+1);
	printf("writer will leave!\n");
	sleep(20);
	strcpy(buf,"The write write another data now");
	write(fd,buf,strlen(buf)+1);
	close(fd);
	exit(0);

在procread.c程序中,我们让此进程一次性只读取一个字符。

int n=20;
while(n--)
{
read(fd,buf,1);
buf[strlen(buf)]='\0';
printf("Read content:%s\n",buf);
sleep(2);
}
close(fd);
unlink(FIFO_NAME);
exit(0);

在procread2.c程序中,我们让此进程一次性读完管道内所有数据。

printf("I will read data from FIFO..\n");
read(fd,buf,BUF_SIZE);
printf("Read content:%s\n",buf);
close(fd);
exit(0);

我们首先运行procwrite,然后立马运行procread,可以看到其每次只输出一个字符;接着我们运行procread2,可以看到这个进程一次性读完了管道内剩余的所有数据,而此时再看运行procread的那个终端,可知procread进程发生了阻塞,不过再稍等片刻,可发现它有接着输出字符,这是因为procwrite进程第二次的写操作解除了procread进程的阻塞。

这里只是为大家做一个简单的演示,我们可以在理解上述规则的基础上编写其他程序去验证。

4.有名管道的写规则

如果打开FIFO时,没有加非阻塞标志,那么写操作遵循以下的规则:

(1)如果写入数据的字节数不大于PIPE_BUF(注意,从内核2.6.11开始,其值为65536),Linux将保证写操作的原子性。当管道内的剩余缓冲区大于要写入的数据字节数时,那么将一次性写完数据;否则,进程进入睡眠状态,直至管道内的缓冲区可以容纳要写入的数据为止。

(2)如果写入的数据字节数不大于PIPE_BUF,那么Linux将不保证写操作的原子性。管道内一有空闲的缓冲区,写进程就去写数据,直至所有数据都被写完为止。

如果打开FIFO的时候加入了阻塞标志,那么写操作遵循一下规则:

(1)如果要写入数据的字节数大于PIPE_BUF,那么linux将不会保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。

(2)如果要写入的数据的字节数不大于PIPE_BUF,那么linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写。

IPC under Linux-FIFO(1)

2010年8月20日

上文中我们了解了管道这个基本的通信方式,不过在文章最后我们也总结了管道的相关局限性,比如仅可以在亲缘进程之间进行通信等。本文要说明的IPC通信方式实际上可称作PIPE的加强版:有名管道(FIFO或者named PIPE)。首先“望文思意”,FIFO是具有具体文件名的(具体路径名),因为FIFO是一个设备文件。因此,对于一般文件的操作,比如open,write,read都可以对有名管道进行操作。

1.创建有名管道

使用mkfifo函数就可以了,但是并不会类似PIPE那样:先fork,再mkfifo。还记得吗,任意进程间都可以进行FIFO通信,因此在A程序(进程)中创建了管道,在B程序(进程)中只要打开相应的管道即可。

2.有名管道的打开规则

当我们创建了有名管道时,每次使用前还要用open函数打开这个管道文件。其实这并不是有名管道的什么特殊之处,因为有名管道是存在于磁盘上的文件,而管道是存在内存中的特殊文件。

这里我们需要注意的是,进程以不同的方式打开有名管道,会对其自身产生不同的影响。下面我们一点一点的去了解。

首先,如果进程以O_RDWR方式打开管道,此进程一定不会阻塞。

如果进程A以只写方式打开管道,那么这个进程会一直阻塞到有另外一个进程B以写方式打开管道为止。当然,如果A进程以只读方式打开管道前,B进程就已经以只写方式打开了管道,那么A进程必然不会阻塞。

类似的,如果A进程以只写方式打开管道,而没有其他进程以读方式打开管道,那么A进程也会阻塞。

上面我们所说的规则具有一般性,因为缺省情况下进程是以阻塞方式打开文件的。如果我们打开管道时加入了非阻塞标志(O_NONBLOCK),那么又会产生和上面不一样的情况。具体如下问所述。

如果进程A以只写方式打开管道,此时又无其他进程以只读方式打开管道,那么进程A将返回错误标志ENXIO。进程A以只读方式打开管道的情况与上类似。

如何去验证上述打开规则?《LinuxC编程实战》一书中例10-5就是个很好的验证程序。

如果按照此书所述那样去运行程序,可以发现procwrite会在procread运行前阻塞。如果我们在procwrite.c中以可读写方式打开管道,那么可以发现运行procwrite后立马会运行完毕。而且,我们ls -l一下,可以发现当前目录下多了一个myfifo的管道文件(注意文件存取权限最前面的那个P)。

关于打开规则更多的测试,请参考这里的例子:附录2,对我们的理解有极大帮助。

IPC under Linux-Pipe

2010年8月19日

管道是一种最基本的IPC方式,本文以笔记形式以及典型程序来描述管道的特点。

1.管道特点关键字

  • 半双工通信
  • 亲缘进程间通信
  • 内存中的特殊文件
  • 在末尾写数据,在头部读数据

2.管道创建

先创建管道,再创建进程。即先pipe,再fork。

3.从管道中读数据

  • 管道的写端不存在,那么进程认为已经读到了数据的末尾,读函数返回的字节数为0。
  • 这里有三个名词:PIPE_BUF,请求读取字节数(request_num),管道中数据量(pipe_num);那么关于这三个量之间的关系如下图。

下面这个程序可以加深你对上述读规则的理解,实现代码如下:

int main()
{
	int fd[2];
	int ret,request;
	char rbuf[4096];
	char wbuf[5000];
	char *msg="hello,pipe!",*msg2="hello,I come here ,too!";
	pid_t pid;
	int stat_val;

	if(pipe(fd)\<\0)
	{
		printf("pipe error\n");
		exit(1);
	}

	if((pid=fork())==0)
	{
		//child process write
		close(fd[0]);
		ret=write(fd[1],msg,strlen(msg)+1);
		printf("I am writer and I write %d Byte data\n",ret);
          	sleep(5);
		ret=write(fd[1],msg2,strlen(msg2)+1);
		printf("I am writer and I write %d Byte data\n",ret);
		sleep(5);
		close(fd[1]);
		exit(0);
	}
	else if(pid\>\0)
	{
		//parent process read
		close(fd[1]);
		request=5000;
		ret=read(fd[0],rbuf,request);
		printf("when reader request %d Byte,the actual return Bytes is %d\n",request,ret);
		request=10;
		ret=read(fd[0],rbuf,request);
		printf("when reader requesr %d Byte,the actual return Bytes is %d\n",request,ret);
		request=100;
		ret=read(fd[0],rbuf,request);
		printf("when reader request %d Byte,the actual return Bytes is %d\n",request,ret);
		request=1;
		ret=read(fd[0],rbuf,request);
		printf("when reader request %d Byte,the actual return Bytes is %d\n",request,ret);
		close(fd[0]);
		exit(0);
	}
}

该程序中父进程读数据,子进程去写数据。至于运行过程请你亲自去尝试,因为我觉得我在这里说的多么具体,都不及你亲自去感受。

首先你可能会想:是否应该在父进程所要执行的代码前加一个sleep(1)?因为在父进程读之前,管道内至少得有数据啊!其实可以不用加。因为如果管道内无数据可读,父进程自动阻塞,直至有数据或者不存在写端时才会继续运行。

在子进程第一次写操作后,就睡眠5秒,此时父进程刚好读完管道内的所有数据,因为请求的数据量为5000,而PIPE_BUF为4960。现在管道内已经无数据可读了,因此父进程阻塞。5秒过后,子进程继续写入24Byte。父进程第二次请求10Byte,因此根据上述我们所说的读规则,read函数返回10。父进程第三次请求读100Byte的数据,根据上述读规则,他将返回管道内所有数据的字节数,因此read函数返回14Byte。

父进程第四次请求读前,管道已经空了,按照我们刚所说的,父进程现在应该阻塞了。因此在子进程睡眠5秒后,父进程发现读端已经关闭,因此读函数返回0。

4.从管道中写数据

  • 只有管道读端存在时,管道写数据才有意义。
  • 写数据时,Linux不能保证写入的原子性。

关于写数据的第一条规则,很好去验证,比如上述程序,如果在父进程一开始就关闭两个端,那么子进程将会自动退出。但是如果将子进程的读端不关闭,那么子进程是可以正常写入pipe的。因此,在写管道时,应该至少保证一个进程的的读端是打开的。

关于写数据的非原子性是指,如果要写入A字节的数据,而此时管道内只有B字节的缓冲区,而且A>B。那么此时写端的进程就会分批写入数据:先写入B字节,然后再写入(A-B)字节。特殊的,当A-B仍然大于PIPE_BUF的时候,那么还是先写PIPE_BUF字节,然后再写(A-B-PIPE_BUF)字节的数据。如果当前在写入最后一批数据前,可以一次性写入管道,那么此时写函数将返回写入的字节数,而不是等所有数据都写完毕后再返回写入的字节数。

下面这个程序请务必亲自动手验证。

int main()
{
	int fd[2];
	int ret,request;
	char rbuf[50000];
	char wbuf[65000];
	char wbuf2[100000];
	char *msg="hello,pipe!",*msg2="hello,I come here ,too!";
	pid_t pid;
	int stat_val;

	if(pipe(fd)\<\0)
	{
		printf("pipe error\n");
		exit(1);
	}

	if((pid=fork())==0)
	{
		//child process write
		close(fd[0]);
		ret=write(fd[1],wbuf,65000);
		printf("I am writer and I write %d Byte data\n",ret);
		ret=write(fd[1],wbuf2,100000);
		printf("I am writer and I write %d Byte data\n",ret);
		printf("writer has over\n");
		close(fd[1]);
		exit(0);
	}
	else if(pid\>\0)
	{
		//parent process read
//		sleep(3);
		close(fd[1]);
		while(1)
		{
			sleep(1);
			ret=read(fd[0],rbuf,10000);
			printf("I am reader and the num of reading data is %d\n",ret);
		}
		close(fd[0]);
		exit(0);
	}
}

在程序运行后,当第七次显示已经读了10000字节时,就说明了写管道时的非原子性。因为如果写是原子性的,即便先写入的65000字节被读走,那么也不可能一次性全部写入100000字节(因为PIPE_BUF为65536)。当第七次提示已读入10000字节时,说明先将一部分数据写入了管道内的空闲区,否则第七次读入提示只显示已读入5000字节。 当第11次读入提示显示后,就会接着显示已写入100000字节的提示。因为前11次读中:先用7次(第七次的10000字节中既有第一次写入的5000字节,又有第二次的一部分数据)读走了第一次写入的65000字节,然后用5次读走第二次要写入100000字节的前45000字节,当剩余65000字节时,已经可以一次性写入管道,因此在最后一次写之前返回写入的字节数100000。这与上述我们概述的原理符合。

以上解释需要多次实验和斟酌。

5.管道的局限性关键词

  • 单向流动
  • 缓冲区局限性
  • 亲缘进程间通信局限性
  • 字符流传送

对于管道半双工流动的特点,如果使用两条管道,那么就可以实现两个进程间全双工通信。具体例子《LinuxC编程实战》一书中有详解。不过我这里想特别说明的是,由于管道的读端在无数据可读时会自动阻塞,因此如果两个管道都首先进行读数据,那么此程序会发生死锁,因为父子进程都在等待数据的出现(当然它们等待的管道不同)。因此解决的办法是父子进程都先去写数据或和一个先读一个先写。

对于管道的另一个应用,就是父进程创建子进程后向子进程传递参数(具体可见《实战》P250)。对于这个应用,下面仅说明如何传递。运行监控端程序时,会输入相应的参数(argv[1]),父进程将其写入管道。按照以往的读管道规则,子进程直接从管道读就可以了,但是在本应用中,子程序会去执行一个新的程序(ctrlpocess.c)。我们知道子进程在执行exec函数后,它的“前身”除了pid,几乎一点都不留。所以此刻在新进程中我们直接使用fd[0],fd[1]显然不行。

现在该如何解决?我们既要从管道中读数据,还得保证有一个可用的文件描述符。显然我们应该在子进程“离别”前(执行exec前)将读端(fd[0])定向到标准输入端。这样子进程在执行新的程序时就可以在标准出入端读数据了。

最后需要说明的是,本文仅仅阐述了管道部分的基本知识点,要深入理解管道则需要多次实践。毕竟实践出真知。

list.h的简单应用

2010年8月18日

既然我们分析了list.h,那么就要学以致用,因为通过具体的例子我们才能真实感受到双链表的用法。本文首先为你呈现一个最基本的双链表使用方法,然后在引用另外两个例子,大家可以去亲自试试。

1.简单的学生管理系统

这里学生管理系统只是个基本模型:用户输入数据,然后通过输出重定向到stuInfo.txt文件当中。先看学生信息数据结构:

struct postinfo
{
	char id[20];
	char name[20];
	char sex[10];
	char addr[50];
	char email[20];
	struct list_head list;
};

正如我们前面所说的那样,整个双链表是通过struct list_head结构链接起来的,这样我们每次对某个结点的操作,都是先获得list字段的地址,进而通过list_entry宏获得当前结点的地址。

首先,创建头指针。注意我这里说的是头指针,并不是头结点,因此你应该可以理解为什么下面会首先创建一个struct list_head类型的变量,而不是struct postinfo类型的变量。

	struct list_head head;
	INIT_LIST_HEAD(&head);

创建好头指针,我们接下来就去增加学生信息。每次都先生成一个struct postinfo类型的变量tmp,再将每次要增加的信息都暂时保存在此变量中,接着就利用上次我们所说的增加函数:

list_add_tail(&(tmp->list),head);

注意这里我们使用的是&(tmp->list),正如我们一开始所说的每次对结点的操作实际上都是通过list字段去完成的。

添加完毕后,如何去打印数据?使用我们的遍历宏。pos只是一个struct list_head类型的指针,这个宏会首先使得pos指向head->next,即list链表的第一个结点(而非第一个学生信息结点)。每次移动链表后,通过pos获得当前结点的地址,那么就可以获得其他数据字段的地址了。

	list_for_each(pos,head)
	{
		pinfo=list_entry(pos,struct postinfo,list);
		printf("%s %s %s %s %s\n",pinfo->id,pinfo->name,pinfo->sex,pinfo->addr,pinfo->email);
        }

这个演示程序关键部分就是这样,其他地方只要你有C语言基础,就可以完成的。

2.遍历进程

具体过程请点击这里

list.h头文件分析(2)

2010年8月13日

Last Update:8/27

Last Update:8/15

9.合并链表

既然我们可以切割链表,那么当然也可以合并了。先看最基本的合并函数,就是将list这个链表(不包括头结点)插入到prev和next两结点之间。这个代码阅读起来不困难,基本上是“见码知意”。

 271static inline void __list_splice(const struct list_head *list,
 272                                 struct list_head *prev,
 273                                 struct list_head *next)
 274{
 275        struct list_head *first = list->next;
 276        struct list_head *last = list->prev;
 277
 278        first->prev = prev;
 279        prev->next = first;
 280
 281        last->next = next;
 282        next->prev = last;
 283}

理解了最基本的合并函数,那么将它封装起来,就可以形成下面两个函数了,分别在head链表的首部和尾部合并。这里的调用过程类似增加,删除功能。

 290static inline void list_splice(const struct list_head *list,
 291                                struct list_head *head)
 292{
 293        if (!list_empty(list))
 294                __list_splice(list, head, head->next);
 295}

 302static inline void list_splice_tail(struct list_head *list,
 303                                struct list_head *head)
 304{
 305        if (!list_empty(list))
 306                __list_splice(list, head->prev, head);
 307}

合并两个链表后,list还指向原链表,因此应该初始化。在上述两函数末尾添加初始化语句INIT_LIST_HEAD(list);后,就安全了。

10.遍历

下面我们要分析链表的遍历。虽然涉及到遍历的宏比较多,但是根据我们前面分析的那样,掌握好最基本的宏,其他宏就是进行“封装”。便利中的基本宏是:

381#define __list_for_each(pos, head) \
382        for (pos = (head)->next; pos != (head); pos = pos->next)

head是整个链表的头指针,而pos则不停的往后移动。但是你有没有觉得,这里有些奇怪?因为我们在上篇文章中说过,struct list_head结构经常和其他数据组成新的结构体,那么现在我们只是不停的遍历新结构体中的指针,如何得到其他成员?因此我们需要搞懂list_entry这个宏:

 348#define list_entry(ptr, type, member) \
 349        container_of(ptr, type, member)

这个宏的作用是通过ptr指针获取type结构的地址,也就是指向type的指针。其中ptr是指向member成员的指针。这个list_entry宏貌似很简单的样子,就是再调用container_of宏,可是当你看了container_of宏的定义后……

 443#define container_of(ptr, type, member) ({                      \
 444        const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
 445        (type *)( (char *)__mptr - offsetof(type,member) );})

是不是让人有点抓狂?别急,我们一点点来分析。

首先这个宏包含两条语句。第一条:const typeof( ((type *)0)->member ) *__mptr = (ptr);首先将0转化成type类型的指针变量(这个指针变量的地址为0x0),然后再引用member成员(对应就是((type *)0)->member ))。注意这里的typeof(x),是返回x的数据类型,那么 typeof( ((type *)0)->member )其实就是返回member成员的数据类型。那么这条语句整体就是将__mptr强制转换成member成员的数据类型,再将ptr的赋给它(ptr本身就是指向member的指针)。

第二句中,我们先了解offsetof是什么?它也是一个宏被定义在:linux/include/stddef.h中。原型为:

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER);

这个貌似也很抓狂,不过耐心耐心:((TYPE *)0)->MEMBER)这个其实就是提取type类型中的member成员,那么&((TYPE *)0)->MEMBER)得到member成员的地址,再强制转换成size_t类型(unsigned int)。但是这个地址很特别,因为TYPE类型是从0x0开始定义的,那么我们现在得到的这个地址就是member成员在TYPE数据类型中的偏移量。

我们再来看第二条语句, (type *)( (char *)__mptr – offsetof(type,member) )求的就是type的地址,即指向type的指针。不过这里要注意__mptr被强制转换成了(char *),为何要这么做?因为如果member是非char型的变量,比如为int型,并且假设返回值为offset,那么这样直接减去偏移量,实际上__mptr会减去sizeof(int)*offset!这一点和指针加一减一的原理相同。

有了这个指针,那么就可以随意引用其内的成员了。关于此宏的更具体了解,不妨亲自动手测试这里的程序。

好了,现在不用抓狂了,因为了解了list_entry宏,接下来的事情就很简单了。

下面这个宏会得到链表中第一个结点的地址。

 359#define list_first_entry(ptr, type, member) \
 360        list_entry((ptr)->next, type, member)

真正遍历的宏登场了,整个便利过程看起来很简单,可能你对prefetch()陌生,它的作用是预取节点,以提高速度。

 367#define list_for_each(pos, head) \
 368        for (pos = (head)->next; prefetch(pos->next), pos != (head); \
 369                pos = pos->next)

我们再来看一开始我们举例的那个便利宏。注意它和上述便利宏的区别就是没有prefetch(),因为这个宏适合比较少结点的链表。

 381#define __list_for_each(pos, head) \
 382        for (pos = (head)->next; pos != (head); pos = pos->next)

接下来这个遍历宏貌似长相和上面那几个稍有不同,不过理解起来也不困难,倒着(从最后一个结点)开始遍历链表。

389#define list_for_each_prev(pos, head) \
 390        for (pos = (head)->prev; prefetch(pos->prev), pos != (head); \
 391                pos = pos->prev)

下面两个宏是上述两个便利宏的安全版,我们看它安全在那里?它多了一个与pos同类型的n,每次将下一个结点的指针暂存起来,防止pos被释放时引起的链表断裂。

399#define list_for_each_safe(pos, n, head) \
 400        for (pos = (head)->next, n = pos->next; pos != (head); \
 401                pos = n, n = pos->next)

 409#define list_for_each_prev_safe(pos, n, head) \
 410        for (pos = (head)->prev, n = pos->prev; \
 411             prefetch(pos->prev), pos != (head); \
 412             pos = n, n = pos->prev)

前面我们说过,用在list_for_each宏进行遍历的时候,我们很容易得到pos,我们都知道pos存储的是当前结点前后两个结点的地址。而通过list_entry宏可以获得当前结点的地址,进而得到这个结点中其他的成员变量。而下面两个宏则可以直接获得每个结点的地址,我们接下来看它是如何实现的。为了方便说明以及便于理解,我们用上文中的结构struct stu来举例。pos是指向struct stu结构的指针;list是一个双链表,同时也是这个结构中的成员,head便指向这个双链表;member其实就是这个结构体中的list成员。

在for循环中,首先通过list_entry来获得第一个结点的地址;&pos->member != (head)其实就是&pos->list!=(head);它是用来检测当前list链表是否到头了;最后在利用list_entry宏来获得下一个结点的地址。这样整个for循环就可以依次获得每个结点的地址,进而再去获得其他成员。理解了list_for_each_entry宏,那么list_for_each_entry_reverse宏就显而易见了。

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

 431#define list_for_each_entry_reverse(pos, head, member)                  \
 432        for (pos = list_entry((head)->prev, typeof(*pos), member);      \
 433             prefetch(pos->member.prev), &pos->member != (head);        \
 434             pos = list_entry(pos->member.prev, typeof(*pos), member))

下面这两个宏是从当前结点的下一个结点开始继续(或反向)遍历。

 456#define list_for_each_entry_continue(pos, head, member)                 \
 457        for (pos = list_entry(pos->member.next, typeof(*pos), member);  \
 458             prefetch(pos->member.next), &pos->member != (head);        \
 459             pos = list_entry(pos->member.next, typeof(*pos), member))

 470#define list_for_each_entry_continue_reverse(pos, head, member)         \
 471        for (pos = list_entry(pos->member.prev, typeof(*pos), member);  \
 472             prefetch(pos->member.prev), &pos->member != (head);        \
 473             pos = list_entry(pos->member.prev, typeof(*pos), member))

与上述宏不同的是,这个宏是从当前pos结点开始遍历。

 483#define list_for_each_entry_from(pos, head, member)                     \
 484        for (; prefetch(pos->member.next), &pos->member != (head);      \
 485             pos = list_entry(pos->member.next, typeof(*pos), member))

接下来几个宏又分别是上述几个宏的安全版。安全原因上面已经说过,在此不再赘述。

list_for_each_entry_safe(pos, n, head, member)
list_for_each_entry_safe_continue(pos, n, head, member)
list_for_each_entry_safe_from(pos, n, head, member)
list_for_each_entry_safe_reverse(pos, n, head, member)

以上即是list.h头文件中的大部分内容分析。关于hash表部分在此暂不分析。

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