日志标签 ‘IPC’

生产者与消费者模型的实现

2010年8月29日

学习了信号量以及共享内存后,我们就可以实现进程的同步与互斥了。说到这里,最经典的例子莫过于生产者和消费者模型。下面就和大家一起分析,如何一步步实现这个经典模型。完整代码可以在这里下载

下面程序,实现的是多个生产者和多个消费者对N个缓冲区(N个货架)进行访问的例子。现在先想想我们以前的伪代码是怎么写的?是不是这样:

//生产者:
	while(1)
	{
		p(semid,1);
		sleep(3);
		p(semid,0);
		//producer is producing a product
		goods=rand()%10;//product a goods
		shmaddr[indexaddr[0]]=goods;//The goods is placed on a shelf
		printf("producer:%d produces a product[%d]:%d\n",getpid(),indexaddr[0],goods);
		indexaddr[0]=(indexaddr[0]+1)%10;
		v(semid,0);
		sleep(3);
		v(semid,2);
	}
//消费者:
	while(1)
	{
		p(semid,2);
		sleep(1);
		p(semid,0);
		//consumer is consuming a product
		goods=shmaddr[indexaddr[1]];//The goods on the shelf is taken down
		printf("consumer:%d consumes a product[%d]:%d\n",getpid(),indexaddr[1],goods);
		indexaddr[1]=(indexaddr[1]+1)%num;
		v(semid,0);
		sleep(1);
		v(semid,1);
	}

可能上面的代码你有些眼熟,又有些困惑,因为它和课本上的代码不完全一样,其实上面的代码就是伪代码的linuxC语言具体实现。我们从上面的代码中慢慢寻找伪代码的踪迹:p(semid,0)和v(semid,0)的作用是让进程互斥访问临界区。临界区中包含的数据indexaddr[0],indexaddr[1],以及shmaddr数组分别对应伪代码中的in,out,buffer。p(semid,1)和v(semid,2)以及p(semid,2)和v(semid,1)实现的是同步作用。

并且,在生产者中,生产者生产了一个货物(goods=rand()%10;),然后将这个货物放上货架(shmaddr[indexaddr[0]]=goods;)。在消费者中,消费和从货架上取下货物(goods=shmaddr[indexaddr[1]];)。

好了,现在再看一边上面的代码,我想你的思路就清晰了。

了解了核心代码,并不能算就完成了生产者和消费者模型,因为生产者和消费者核心代码前还得做一些些准备工作,具体要准备些什么,我们具体来分析。

首先申请一块共享内存,这块共享内存用于存放生产者所生产的货物。同时我们可以看到这块共享内存大小为10字节。这里需要注意,每个生产着或消费者运行后,都要去试着分配这样的一块共享内存。如果在当前进程运行前已经有某个进程已经创建了这块共享内存,那么这个进程就不再创建(此时createshm会返回-1并且错误代码为EEXIST),只是打开这块共享内存。创建后,再将这块共享内存添加到当前进程的地址空间。

	num=10;
	//create a shared memory as goods buffer
	if((shmid_goods=createshm(".",'s',num))==-1)
	{
		if(errno==EEXIST)
		{
			if((shmid_goods=openshm(".",'s'))==-1)
			{
				exit(1);
			}
		}
		else
		{
			perror("create shared memory failed\n");
		        exit(1);
		}
	}
	//attach the shared memory to the current process
	if((shmaddr=shmat(shmid_goods,(char*)0,0))==(char*)-1)
	{
		perror("attach shared memory error\n");
		exit(1);
	}

接下来还要再申请一块共享内存,用于存放两个整形变量in和out(其实就是申请一个含有2个整形变量的数组而已)。他们记录的是生产和消费货物时“货架”的索引。与上面情况相同,如果已经有其他进程创建了此块共享内存,那么当前进程只是打开它而已。

注意这里对两个整形变量的初始化时的值均为0。

	//create a shared memory as index
	if((shmid_index=createshm(".",'z',8))==-1)
	{
		if(errno==EEXIST)
		{
			if((shmid_index=openshm(".",'z'))==-1)
			{
				exit(1);
			}
		}
		else
		{
			perror("create shared memory failed\n");
		        exit(1);
		}
	}
	else
	{
		is_noexist=1;
	}
	//attach the shared memory to the current process
	if((indexaddr=shmat(shmid_index,(int*)0,0))==(int*)-1)
	{
		perror("attach shared memory error\n");
		exit(1);
	}
	if(is_noexist)
	{
		indexaddr[0]=0;
		indexaddr[1]=0;
	}

接下来就是创建一个信号量集,这个信号量集中包含三个信号量。第一个信号量实现的互斥作用,即进程对临界区的互斥访问。剩下两个均实现的是同步作用,协调生产者和消费者的合理运行,即货架上没有空位时候生产者不再生产,货架上无商品时消费者不再消费。

注意下面对每个信号量的赋值情况。互斥信号量当然初值为1。而同步信号量两者之和不能大于num的值。

	//create a semaphore set including 3 semaphores
	if((semid=createsem(".",'t',3,0))==-1)
	{
		if(errno==EEXIST)
		{
			if((semid=opensem(".",'t'))==-1)
			{
				exit(1);
			}
		}
		else
		{
			perror("semget error:");
			exit(1);
		}
	}
	else
	{
		union semun arg;
	        //seting value for mutex semaphore
         	arg.val=1;
         	if(semctl(semid,0,SETVAL,arg)==-1)
         	{
			perror("setting semaphore value failed\n");
	        	return -1;
            	}
          	//set value for synchronous semaphore
	        arg.val=num;
            	//the num means that the producer can continue to produce num products
          	if(semctl(semid,1,SETVAL,arg)==-1)
            	{
			perror("setting semaphore value failed\n");
	         	return -1;
         	}
           	//the last semaphore's value is default
           	//the default value '0' means that the consumer is not use any product now
      	}

基本上这样,就算完成了生产者和消费者的前期工作。我们可以看到,在核心代码中,我们只需要“装模作样”的将代码“各就各位”即可,当然这需要你理解生产者消费者这个基本模型。而在下面的准备代码中,则需要我们理解关于信号量和共享内存的一些基本函数。

最后再说说使用,建议先运行一个生产者和一个消费者,观察两者是如何协调工作的。然后再只运行一个生产者或一个消费者,看其是否会阻塞。了解了以上情况后,你就可以同时运行多个生产者和消费者了。

信号量集实现进程互斥

2010年8月26日

本文为大家呈现的代码可以实现进程的互斥。下面代码省去了pv操作的的具体定义以及union semun的定义(可参考前文)。本程序前半部分是一些初始化工作:生成key,创建信号量集,设置信号量初值。接下来的代码依次生成N个子进程,具体数目由运行时的参数决定。

现在我们要关注的是,这几个子进程如何互斥的访问临界区?这是本问要说明的重点。请先看本程序加PV操作和不加PV操作的结果:

edsionte@edsionte-laptop:~/code/IPC$ ./pv 3
===process 2164 enter the critical section===
===process:2164 is accessing=================
===process 2164 leave the critical section===
===process 2165 enter the critical section===
===process:2165 is accessing=================
===process 2165 leave the critical section===
===process 2166 enter the critical section===
===process:2166 is accessing=================
===process 2166 leave the critical section===
//不加PV
===process 2175 enter the critical section===
===process 2176 enter the critical section===
===process 2177 enter the critical section===
===process:2177 is accessing=================
===process:2176 is accessing=================
===process:2175 is accessing=================
===process 2176 leave the critical section===
===process 2177 leave the critical section===
===process 2175 leave the critical section===

可以看到加入pv操作后,子进程在访问临界区时都不受子其他进程的影响。

可是仔细想一下,这个程序真的是在演示多个进程互斥访问同一个临界区吗?当然不是,其实从程序中的fork函数就可以发现。因为fork后的子进程代码段,数据段等都是父进程的副本,因此下面的程序根本不存在所谓的多个进程同时访问一个临界区。其实就是每个进程在唱自己的独角戏(各进程访问属于自己的临界区,一对一)而已。

但是,另一个事实又摆在我们面前:如果不加PV,那么很明显每个进程访问临界区又是交互运行的。其实我们可以这样想,对于单个子进程A而言,即便没有其他进程要竞争A进程要访问的那个临界区,那么A进程要也要装作周围有很多进程那样——在访问临界区的时候不受其他进程的干扰——只不过A进程现在只是独自一人去遵守那个规则而已。我们可以在V操作后再加一句话:

			sleep(2);
			printf("===process %d is doing another thing=======\n",getpid());

再次运行,你可以发现这句话并不总是紧挨在临界区之后就被执行的,因为出了临界区,这几个子进程就是普通的交互运行而已。

所以,虽然这并不是我们以前熟悉的那个多进程互斥使用一个临界区的场景,但也可以体现进程对临界区的互斥访问。这样说来可能会有点绕,但是仔细想想也应该会理解。

那么如何实现多个进程互斥访问一个临界区?你也许会想到vfork函数,可是比较糟糕的是vfork后,父进程在子进程退出前总是阻塞,这样并不适合我们这里依次生成多个子进程的情况。如果还感到困惑,那么也没关系,不妨在学习了共享内存之后,再来理解本文。用共享内存才可以实现多个进程都访问一个内存区

int main(int argc,char** argv)
{
	int proj_id;
	int semid;
	union semun arg;
	pid_t pid;
	key_t key;
	int num;
	int i,j;

	if(argc!=2)
	{
		printf("error:%s num\n",argv[0]);
		return -1;
	}

	num=atoi(argv[1]);

	//create key
	proj_id=2;
	if((key=ftok(".",proj_id))==-1)
	{
		printf("generating IPC key failed\n");
		return -1;
	}

	//create a semaphore set
	if((semid=semget(key,1,IPC_CREAT|0666))==-1)
	{
		printf("creating semaphore set failed\n");
		return -1;
	}

	arg.val=1;
	if(semctl(semid,0,SETVAL,arg)==-1)
	{
		printf("set semval failed\n");
		return -1;
	}

	for(i=0;i<\num;i++)
	{
		pid=fork();
		if(pid<0)
		{
			printf("creating new process failed\n");
			return -1;
		}
		else if(pid==0)
		{
			if((semid=semget(key,1,0))==-1)
			{
				printf("geting semid failed in the child process\n");
				return -1;
			}

			p(semid,0);
			printf("===process %d enter the critical section===\n",getpid());
           		sleep(1);
			printf("===process:%d is accessing=================\n",getpid());
			sleep(1);
			printf("===process %d leave the critical section===\n",getpid());
			sleep(1);
			v(semid,0);

			return 0;
		}
	}

	for(i=0;i<\num;i++)
	{
		wait(NULL);
	}

	if(semctl(semid,0,IPC_RMID,0)==-1)
	{
		printf("remove the sem set failed\n");
		return -1;
	}

	return 0;
}

IPC under Linux-Semaphore

2010年8月25日

本文将为你说明IPC的另一种方式:信号量。在操作系统原理课程中,我们对信号量机制有所了解,即实现进程对共享资源的互斥访问以及进程间的同步。与其他几种通信方式相比,信号量更突出的是对共享资源的访问控制。接下来,我们一边回忆以前学过的信号量机制,一边学习如何用系统调用函数来实现相关操作。

1.创建信号量集

注意这里使用的是信号量集,而不是创建一个信号量。通常我们用伪代码实现进程间的互斥或同步时,都定义一个整形变量作为信号量,初值一般都为0或1。实际应用时,我们每次都需要创建一个信号量集,即使此集合只包含一个信号量。一般我们通过下面函数去创建或者打开一个信号量集。

int semget(key_t key,int nsems,int semflg);

我们这里需要注意的是,此函数何时创建一个信号量集,何时打开一个信号量集。当semflg=IPC_CREATE时,如果当前系统中不存在此信号量集合(key值不存在),那么semget函数完成一个信号量的创建;否则,semget函数打开这个已存在的信号量集。当semflg=IPC_CREATE|IPC_EXCL时,只会完成创建,如果key值对应的信号量集合以存在,那么直接返回错误,错误代码为EEXIST。这并不难理解,和open文件的情况类似。此函数成功执行返回信号量集的标示符,否则为-1。

另外关于我们所创建的这个信号量集,有结构体struct semid_ds与之对应,这个结构体中存储信号量集中的相关属性。

2.PV操作的实现

对于信号量机制,我们最熟悉的应该便是PV操作了。以前我们在写进程互斥或同步的伪代码时,都是直接来使用P(semaphore)和V(semaphore)。那么我们如何具体实现?

int semop(int semid,struct sembuf *sops,size_t nsops);

现在我要告诉你,P和V操作均通过上述函数来完成。你肯定马上会想,P和V两个截然不同的操作如何使用上述函数来完成?这其实取决于sops参数。请先看这个参数的类型:

struct sembuf
{
	ushort sem_num;/*信号在信号量集中的索引*/
	short  sem_op; /*操作类型,是P还是V*/
	short  sem_flg;/*操作标志*/
}

我们可以发现,通过semid和sem_num两个字段就可以确定信号量集中的指定信号量。sem_op取不同的值就会产生不同的操作。特别的,如果其值为0,则此时semop操作的作用是测试信号量的值是否为0。下面的代码是经典PV操作的实现方法。

int p(int semid,int index)
{
	struct sembuf buf={0,-1,IPC_NOWAIT};

	if(index<0)
	{
		printf("error:index of the semaphore is invalid\n");
		exit(1);
	}
	buf.sem_num=index;
	if(semop(semid,&buf,1)==-1)
	{
		printf("P operation failed\n");
		exit(1);
	}
	return 0;
}

int v(int semid,int index)
{
	struct sembuf buf={0,+1,IPC_NOWAIT};

	if(index<0)
	{
		printf("error:index of the semaphore is invalid\n");
		exit(1);
	}
	buf.sem_num=index;
	if(semop(semid,&buf,1)==-1)
	{
		printf("V operation failed\n");
		exit(1);
	}
	return 0;
}

我们这里特别要说明的是struct sembuf结构中的sem_flg字段,如果其值为IPC_NOWAIT,根据sem_op的不同意义也不同。当sem_op为0时,如果设置IPC_NOWAIT,那么调用进程直接返回;否则调用进程进入睡眠状态直至信号量为0。当sem_op<0时,表示申请资源,如果没有设置IPC_NOWAIT,那么调用进程阻塞至此资源被释放;否则进程直接返回。

3.信号量的控制

上述两部分内容——如何创建信号量集,如何PV操作——似乎已经可以诠释信号量了,但是这毕竟不是我们写伪代码,我们必须对信号量进行相关控制,比如如何对信号量设置初值,如何得到信号量当前的值等等。因此掌握好控制函数semctl尤为重要。

int semctl(int semid,int semnum,int cmd,union semun);

注意第四个参数类型是不确定的,由cmd的类型来确定。第四个参数所有可能会用到的变量都被定义在一个共用体中。

union semun
{
	int val;
	struct semid_ds *buf;
	ushort *array;
	void *pad;
}

下面我们对几种常用的cmd进行说明。

如果我们想打印某个已存在信号量集的各个属性信息时,那么可以参考下面代码。

union semun semop;
struct semid_ds seminfo;
semop.buf=&seminfo;
if(semctl(semid,0,IPC_STAT,semop)==-1)
{
	printf("error:get the info of semaphroe failed\n");
	exit(1);
}

因为此时候cmd为IPC_STAT,那么semctl会自动从semop中取走buf字段。此操作成功完成后,信号量集的信息便存储在seminfo中。注意,在使用此操作前,一定要将buf指向一个struct semid_ds类型的变量,否则buf将是一个”野指针”。

如果想为某个指定的信号量设置初值,那么下面代码就是实现将标示符为semid的信号量集中索引为index的信号量赋值为5。

union semnu semop;
semop.val=5;
semctl(semid,index,SETVAL,semop);

想得到某个信号量当前的值时,就可以参考下面代码,注意第四个参数为0,因为cmd为GETVAL,所以semctl函数会忽略第四个参数的取值。

int semval;
semval=semctl(semid,index,GETVAL,0);

关于信号量的基本知识就是这些,下篇文章将用上述内容来实现一个进程互斥的例子。

shell管道重定向实现

2010年8月24日

如果你实现过my_shell.c,那么对管道重定向应该有印象。但是本文中所述的管道重定向,将采用最近我们学习的管道相关系统调用函数来实现。随后,也将和大家一起再去回顾当初my_shell.c中是如何实现管道重定向的

对于管道符号,这里只做简单的说明:管道符前命令的输出作为管道符后命令的输入。对于一般命令而言,输入均来自标准输入,而输出则至标准输出。但是为了实现管道重定向,我们先创建管道,然后将管道写端重定向到标准输出,将管道读端重定向到标准输入。当然这两次重定向分别在不同的进程中完成。具体代码实现可参考下面的代码片段。

	if(pipe(fd)==-1)
	{
		printf("my_pipe:can't create a pipe\n");
		exit(1);
	}

	pid=fork();
	if(pid==0)
	{
		close(fd[0]);
		dup2(fd[1],STDOUT_FILENO);
		close(fd[1]);

		if(execlp(argv[1],argv[1],NULL)==0)
		{
			printf("my_pipe:can't execute the first command in the child process\n");
			exit(1);
		}
	}
	else if(pid>\0)
	{
		close(fd[1]);
		dup2(fd[0],STDIN_FILENO);
		close(fd[0]);

		if(execlp(argv[2],argv[2],NULL)==0)
		{
			printf("my_pipe:can't execute the second command in the parent process\n");
			exit(1);
		}
	}
	else
	{
		printf("my_pipe:can't create a new process\n");
		exit(1);
	}

上述代码所实现的管道重定向只支持没有参数的命令,比如./my_pipe ls wc(相当于命令ls | wc)。如果想要使用命令参数,可以在次基础上继续完善。

可以发现,管道是作为前后两个命令的数据缓冲区,各命令的相关输入或输出只是“私下”操作。既然理解了上述代码,那么my_shell.c中的管道的实现代码也就一目了然了。

首先,创建一个子进程,然后在子进程中创建子子进程(pid2=fork())。其实无非是让这两个子进程去分别执行管道符前后的命令。这里应该注意的是此段代码是如何“模拟”管道的。我们可以发现,其实并没有真正的pipe一个管道,而是创建了一个实实在在tempfile文件而已,最后使用完毕再悄悄删除次文件。

再看具体是如何重定向的,在子子进程中,以写方式打开”管道”,将tempfile定向到标准输出。在子进程中,以读方式打开”管道”,将tempfile定向到标准输入。

看到这里,你应该已经发现,这里所用到的原理其实和一开始我们所述的方法是一致的。

			if(pid==0)//child_process
			{
				int pid2;
				int status2;
				int fd2;

				if((pid2=fork())<\0)
				{
					printf("fork2 error\n");
					return;
				}
				else if(pid2==0)//child_child_process
				{
					if(!(find_command(arg[0])))
					{
						printf("%s: command not found\n",arg[0]);
						exit(0);
					}
					fd2=open("/tmp/tempfile",O_WRONLY|O_CREAT|O_TRUNC,0644);
					dup2(fd2,1);
					execvp(arg[0],arg);
					exit(0);
				}

				if(waitpid(pid2,&status2,0)==-1)//waiting for child_child_process
				{
					printf("wait for child_child process error\n");
				}

				if(!(find_command(argnext[0])))
				{
					printf("%s:command not found\n",argnext[0]);
					exit(0);
				}

				fd2=open("/tmp/tempfile",O_RDONLY);
				dup2(fd2,0);
				execvp(argnext[0],argnext);

				if(remove("/tmp/tempfile"))
				{
					printf("remove error\n");
				}
				exit(0);
			}

			

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错误,提醒以后再写。

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