进程间有很多的通信方式,包括:管道(pipe),命名管道(named pipe),信号(signal),消息(message)队列,共享内存,内存映射(mapped memory),信号量以及套接口(socket)。下面就逐一的介绍着几种通信方式。

一、 管道

管道是一种半双工的通信方式,用于具有亲缘关系的进程进行单方向的通信。linux用pipe函数创建一个管道,其函数原型为:

#include <unistd.h>
int pipe(int fd[2]);

参数fd[2]是一个文件描述符数组,fd[1]为写入文件描述符,fd[0]为读出文件描述符。当建立一个管道的时候,可以实现父子进程间的通信,这是父子进程可以对fd的读写文件描述符进行操作,由于管道式一种单方向的通信,因此父子进程中要关闭读或写的文件描述符。下面举个实例进行解释:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/wait.h>
#define BUFS 1024

int main()
{
	int fd[2];
	pid_t pid,pr;
	char buf[BUFS];
	int len;
	int status;
	if(pipe(fd)<0)
	{
		printf("pipe initialization failed.");
		exit(1);
	}
	
	if((pid =fork())<0)
	{
		printf("fail to fork.");
		exit(1);
	}
	
	if(pid > 0) //父进程的程序段
	{
		close(fd[1]); //关闭父进程的写文件描述符
		pr = wait(&status); // 调用wait()函数防止僵尸进程的产生
		printf("I am father!\n");
		len = read(fd[0],buf, BUFS);
		if(len<11)
		{
			printf("read failed.");
			exit(1);
		}
		printf("recv message is %s\n", buf);
		exit(0);
	}
	else //子进程的程序段
	{
		close(fd[0]);//关闭子进程的读文件描述符
		printf("I am child!");
		write(fd[1], "I success!\n", 11);
		exit(0);
	}
	
}

上面程序的输出结果是:

I am child!
I am father!
recv message is: I success!

并且管道可以多次申请,最终在程序结束后由系统收回。下面就是一个父进程与它的两个子进程之间的通信的例子。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>

#define BUFS 1024


int main()
{
	int fd[2];
	int fdsecond[2];
	pid_t pid,pr;
	char buf[BUFS], bufsecond[BUFS];
	memset(buf,0, BUFS);
	int len, lensecond;
	int status;
	pid_t pidsecond, pidsecondr;
	
	if(pipe(fd)<0)
	{
		printf("pipe initialization failed.");
		exit(1);
	}
	
	if((pid =fork())<0)
	{
		printf("fail to fork.");
		exit(1);
	}
	
	if(pid > 0)
	{
		close(fd[1]);
		
		pr = wait(&status);
		printf("I am father!\n");
		len = read(fd[0],buf, BUFS);
		if(len<11)
		{
			printf("read failed.");
			exit(1);
		}
		printf("recv from first child is: %s\n", buf);
		
		
	}
	else
	{
		close(fd[0]);
		printf("I am first child!\n");
		write(fd[1], "I success(1)!\n", 14);
		exit(0);
	}
	if(pipe(fdsecond)<0)
	{
		printf("pipe initialization failed.");
		exit(1);
	}
	if((pidsecond = fork()) < 0)
	{
		printf("fail to fork.");
		exit(1);	
	}
	if(pidsecond > 0)
	{
		close(fdsecond[1]);
		
		pidsecondr = wait(&status);
		printf("I am father!\n");
		lensecond = read(fdsecond[0],bufsecond,BUFS );
		
		printf("recv from second child is: %s\n", bufsecond);
		
	}
	else
	{
		close(fdsecond[0]);
		//sleep(2);
		//sem_wait(&sem_id);
		printf("I am second child!\n");
		write(fdsecond[1], "I success(2)!\n", 14);
		//sem_post(&sem_id);
		exit(0);
	}
	exit(0);
	
}

输出结果为:

I am first child!
I am father!
recv from first child is: I success(1)!

I am second child!
I am father!
recv from second child is: I success(2)!

二、信号量

信号量主要作为不同进程间和同一进程不同线程间的通信。信号量包括无名信号量和有名信号量。首先对无名信号量做一个简单的介绍。无名信号量可以动态启动。其中函数sem_init()用于初始化一个信号量,他带有一个参数N,表示可用的资源数。在初始化一个信号量之后,一个线程在使用一个资源之前必须调用函数sem_wait()或者sem_trywait()等待一个可用的信号量,并在用完资源之后,还需要调用函数sem_pos()来返还资源。下面通过举例来解释信号量的应用。例子是在同一进程中不同线程上实现同步和互斥的。

#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

int num = 0; //设置全局变量
sem_t sem_id; //定义semaphore变量
void * thread_one_func(void *arg)
{
	sem_wait(&sem_id);
	printf("the thread one has the semaphore!");
	int i;
	for(i = 0; i < 5; i++)
		num++;
	printf("number = %d \n", num);
	sem_post(&sem_id);
}

void *thread_two_func(void *arg)
{
	sem_wait(&sem_id); //程序被挂起,直到等到需要的资源
	printf("the thread two has the semaphore!");
	int i;
	for(i =0; i< 5; i++)
		num --;
	printf("number = %d\n", num);
	sem_post(&sem_id); //程序结束的时候,要返还自己占用的资源

}

int main()
{
	pthread_t pid1, pid2;
	sem_init(&sem_id, 0,1);//初始化sem_id;函数中的第二个参数表示线程间通信(如果为1则为进程间通信);第三个参数是可用资源的数目
	pthread_create(&pid1, NULL, thread_one_func, NULL);
	pthread_create(&pid2, NULL, thread_two_func, NULL);
	pthread_join(pid1, NULL);
	pthread_join(pid2, NULL);
	printf("main... \n");
	return 0;
}

上面例子的输出结果为:

the thread one has the semaphore!number = 5 
the thread two has the semaphore!number = 0
main...

如果改变main函数中的两个线程的创建顺序,即

pthread_create(&pid2, NULL, thread_two_func, NULL);
pthread_create(&pid1, NULL, thread_one_func, NULL);

那么输出结果为:

the thread two has the semaphore!number = -5
the thread one has the semaphore!number = 0 
main...

备注:一个进程中的线程的可以设置很多的属性,如:分离状态,继承性,调度策略等等。而且多线程的发生在不加锁或者其他线程同步互斥控制的时候是随机的。举一个比较经典的例子:两个线程分别对全局变量num;进行自加和自减10次,那么该全局变量的最终结果为:

答案应该是-10~10之间。因为自加和自减不是原子操作;他们分别包括:

读取内存到寄存器;

寄存器自增或自减操作;

将寄存器数据读入内存;

其次看下编译器是怎么编译的,某些编译器比如VC在非优化版本中会编译为以下汇编代码:

__asm
{
        mov eax,  dword ptr[i]
        inc eax
        mov dword ptr[i], eax
}

因此,由于两个线程之间的发生是随机的,而且线程具有私有的寄存器和栈内存的,他们对内存的访问不是互斥的,由于他们访问内存的先后顺序的不同因此产生的结果不同。即使给变量num加上volatile的修饰符也没用,因为volatile是确保编译器不会对被其修饰的变量做优化,即每次访问该变量的时候,都要去内存中读取。但是多线程仍会引起对内存写的重复操作。(猜测,因为我在虚拟机下的ubuntu 运行的多线程结果总是一样的,可能是和虚拟机下的ubuntu只有一个cpu的原因有关)。