Linux 进程间通信方式(管道、命名管道、消息队列、信号量、共享内存、套接字)

什么是进程?
在Linux系统中,进程是管理事务的基本的过程。进程拥有自己独立的处理环境和系统资源。进程整个生命可以简单划分为三种状态:

就绪态:
进程已经具备执行的一切条件,正在等待分配CPU的处理时间。
执行态:
该进程正在占用CPU运行。
等待态:
进程因不具备某些执行条件而暂时无法执行的状态。

进程间通信概念
进程是一个独立的资源分配单元,不同进行之间的资源是独立的,不能在一个进程中直接访问另一个进程的资源。所以不同的进程需要进行信息的交互状态的传递等,因此需要进程间通信。LINUX常见的进程间通信如下:
1.管道
2.命名管道
3.消息队列
4.信号量
5.共享内存
6.套接字

1~5都是同一个主机进程间通信。序号6是不同主机(网络)进程间通信;

管道
管道又称无名管道。是一种古老的IPC通信形式,管道的作用正如其名,需要通信的两个进程在管道的两端;管道是一种特殊类型的文件,存在于内核的缓冲区。管道有如下的特点:
1.半双工,数据不能在两段上传数据,数据只能在一个方向流动。
2.管道不是普通的文件,不属于某个文件系统,只存在于内存中。
3.管道没有名字,只能在亲缘关系的父子进程之间通信。

pid_t fork(void);
fork()函数得到的子进程是父进程的复制品,它从父进程处继承了整个进程的地址空间。
返回值
当成功完成时,fork()将返回0给子进程,并将子进程的进程ID返回给父进程。这两个进程都应该继续从fork()函数执行。否则,-1将返回给父进程,不创建子进程,并将errno设置为指示错误。

int pipe(int fildes[2]);
pipe()函数应该创建一个管道,并将两个文件描述符放入参数fildes[0]和fildes[1]中,每个文件描述符都引用管道读写端打开的文件描述。
返回值
成功完成后,返回0;否则,将返回-1并设置errno表示错误,不分配任何文件描述符,不修改fildes的内容。

fork函数执行结果
Linux 进程间通信方式(管道、命名管道、消息队列、信号量、共享内存、套接字)举个简答例子,使用了pipe()函数来创建管道,通过无名管道通信:

#include <stdio.h>
#include <unistd.h>//pipe , fork
#include <sys/types.h>
#include <string.h>//memset

int main(int argc, char const *argv[])
{
	
	int fd_pipe[2];//描述符,0表示为读,1表示为写
	pid_t pid = 0;
	char buf[128] = "minger";
	int n = 0;
	if (pipe(fd_pipe) < 0)
	{
		perror("pipe");
		return -1;
	}

	pid = fork();
	if ( pid < 0)
	{
		perror("fork");
		return -1;
	}

	else if (pid > 0) //父进程
	{
		close(fd_pipe[1]);//关闭管道的写端
		memset(buf,0,sizeof(buf));
		n = read(fd_pipe[0],buf,sizeof(buf)); //从管道读出数据
		printf("Read %d from the pipe: %s\n",n,buf);
	}

	else //子进程
	{
		close(fd_pipe[0]);//关闭管道的读端
		write(fd_pipe[1], buf, strlen(buf)); //向管道写入数据
	}
	return 0;
}

编译结果
Linux 进程间通信方式(管道、命名管道、消息队列、信号量、共享内存、套接字)
在程序中,利用无名管道实现进程通信。子进程向管道写入字符串,父进程从管道中读取字符串。

命名管道
命名管道与管道不同的是,命名管道允许没有亲缘关系的进程进行通信和不相关的进程也能够进行数据交换。

int mkfifo(const char *path, mode_t mode);
mkfifo实用程序应该按照指定的顺序创建由操作数指定的FIFO特殊文件。
参数:
path:文件操作数用作路径参数。
mode:模式参数

写一个小例子测试一下,命名管道通信,创建两个.c文件,读进程和写进程。

读进程

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include<fcntl.h>
#include <string.h>

int main(int argc, char const *argv[])
{
	int fd_read;
	char buf[128] = {0};

	if (mkfifo("FIFO",S_IRUSR|S_IWUSR) != 0); //0666, 0777
	{
		perror("mkfifo");
		//return -1;
	}
	printf("before open\n");
	fd_read = open("FIFO",O_RDONLY); //只读方式打开
	if (fd_read < 0)
	{
		perror("open");
		return-1;
	}
	printf("after open\n");
	memset(buf,0,sizeof(buf));
	read(fd_read,buf,sizeof(buf));
	printf("read from FIFO buf: %s\n",buf);


	return 0;
}


写进程

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include<fcntl.h>
#include <string.h>

int main(int argc, char const *argv[])
{
	int fd_write;
	char buf[128] = "minger";

	if (mkfifo("FIFO",S_IRUSR|S_IWUSR) != 0); // 0666,0777
	{
		perror("mkfifo");
		//return -1;
	}

	fd_write = open("FIFO",O_WRONLY); //只读方式打开
	if (fd_write < 0)
	{
		perror("open");
		return-1;
	}

	write(fd_write,buf,strlen(buf));
	printf("Write from FIFO buf: %s\n",buf);


	return 0;
}

打开两终端,一个终端先运行写进程,然后运行读进程,编译结果如下:

Linux 进程间通信方式(管道、命名管道、消息队列、信号量、共享内存、套接字)
运行结果可以看到,两个没有亲缘关系的进程可以通过FIFO进行通信。在终端下,可以通过ls -alh,查看FIFO文件是否是命名管道文件;如果p开头表示是管道。
Linux 进程间通信方式(管道、命名管道、消息队列、信号量、共享内存、套接字)

消息队列

消息队列是消息的链表,存储在内核中。一个消息队列由一个标识符(即队列ID)来标识。

消息队列的特点
1.消息队列中的消息是有类型和格式的
2.消息队列可以实现消息的随机查询。消息不一定要以先进先出次序读取。可以按消息的类型读取。
3.每个消息队列都有消息队列的标识符,消息队列的标识符在整个系统中是唯一的
4.消息队列允许一个或者多个进程向它写入或者读取消息。

函数原型

创建消息队列
int msgget(key_t key, int msgflg);
功能:
创建或打开消息队列。
参数:
key:IPC键值
msgfig:标识消息队列是否存在。并且(msgflg & IPC_CREAT)是非零的。
IPC_CREAT:创建消息队列是否存在
返回值
成功完成后,msgget()将返回一个非负整数,即消息队列标识符。
否则,它将返回-1并,表示错误。

发送消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:
添加消息队列
参数:
msqid:消息队列的标识符
msgp:待发送的消息队列的地址
msgsz:消息队列正文的字节数
msgflg:如果(msgflg & IPC_NOWAIT)非零,则不发送消息,调用线程应立即返回。
如果(msgflg & IPC_NOWAIT)为0,调用线程将暂停执行;

返回值
成功完成后,msgsnd()返回0;
否则,将不发送任何消息,msgsnd()将返回-1,并将errno设置为指示错误。

消息队列的消息格式
struct mymsg {
long mtype; //消息类型.
char mtext[1]; //消息文本。
};
结构成员mtype是一种非零正长类型,可由接收流程用于消息选择。
结构成员mtext是长度为msgsz字节的任何文本。参数msgsz的范围可以从0到系统设置的最大值。

接收消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
功能:
读取消息
参数:
msqid:消息队列的标识符。
msgp: 存放消息结构体的地址。
msgsz:消息正文的字节数。
msgtyp:消息类型,有几种类型:
如果msgtyp为0,则接收队列上的第一个消息。
如果msgtyp大于0,则接收msgtyp类型的第一条消息。
如果msgtyp小于0,则接收小于或等于msgtyp绝对值的最低类型的第一条消息。

msgflg:如果(msgflg & IPC_NOWAIT)非零,调用线程应立即返回,返回值为-1;
如果(msgflg & IPC_NOWAIT)为0,调用线程将暂停执行。

返回值
成功完成后,msgrcv()将返回一个与实际放入缓冲区mtext中的字节数相等的值。
否则,将不接收任何消息,msgrcv()将返回-1,并将errno设置为指示错误。

消息队列的控制
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
功能:
控制消息队列,如修改消息队列的属性,或者删除消息队列等。
参数:
msqid:消息队列的标识符
cmd:函数的控制功能
buf:msqid_ds 数据类型的地址
返回值
成功完成后,msgctl()返回0;
否则,它将返回-1并,表示错误。

获得IPC键值
key_t ftok(const char *path, int id);
功能:
获得唯一的IPC键值
参数:
path:路径名
id:项目ID
返回值
成功完成后,ftok()将返回一个密钥。
否则,ftok()将返回(key_t)-1,表示错误。

有了这些函数原型,接下来就写消息队列简单例子:一个任务任务负责发送消息,另一个任务负责接收,通过ftok()创建子进程实现多任务。

message_read.c

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>

typedef struct msg
{
	long mtype; //消息类型
	char ntext[128];//消息正文
}MSG;

int main(int argc, char const *argv[])
{
	key_t key;
	int msgqid;
	MSG my_msg;
	

	if ((key = ftok(".",2019)) == -1)
	{
		perror("ftok");
		exit(-1);

	}
	printf("message queue key: %d\n",key );

	if ((msgqid = msgget(key,IPC_CREAT|0666)) == -1)//打开消息队列
	{
		perror("message");
		exit(-1);
	}
	printf("msgqid: %d\n",msgqid);//打印消息队列ID

	msgrcv(msgqid,&my_msg,sizeof(my_msg.ntext),111,0);
	printf("my_msg.ntext= %s\n",my_msg.ntext);
	msgctl(msgqid,IPC_RMID,NULL);//删除由msgqid指示的消息队列


	return 0;
}

message_write.c

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>

typedef struct msg
{
	long mtype; //消息类型
	char ntext[128];//消息正文
}MSG;

int main(int argc, char const *argv[])
{
	key_t key;
	int msgqid;
	MSG my_msg;
	

	if ((key = ftok(".",2019)) == -1)
	{
		perror("ftok");
		exit(-1);

	}
	printf("message queue key: %d\n",key );

	if ((msgqid = msgget(key,IPC_CREAT|0666)) == -1)//打开消息队列
	{
		perror("message");
		exit(-1);
	}
	printf("msgqid: %d\n",msgqid);//打印消息队列ID
	my_msg.mtype = 111;
	strcpy(my_msg.ntext,"hello minger");
	msgsnd(msgqid,&my_msg,sizeof(my_msg.ntext),0);
	

	return 0;
}

刚开始编译的时候没有运行,查看消息队列 ipcs -q,显示已用字节数为 0,消息为0
Linux 进程间通信方式(管道、命名管道、消息队列、信号量、共享内存、套接字)
编译结果
Linux 进程间通信方式(管道、命名管道、消息队列、信号量、共享内存、套接字)

信号量

信号量是一个计数量,它主要用在多个进程需要对共享数据进行访问的时候。用于进程或线程间的同步和互斥。那么借助信号量就可以完成这样的事情。
流程及特点
操作系统的P操作就是上锁,V操作就是解锁
如果信号量值大于0,则资源可用,并且将其减1,表示当前已被使用
如果信号量值为0,则进程休眠直至信号量值大于0

函数原型

信号初始化
int sem_init(sem_t *sem, int pshared, unsigned value);
功能:
创建一个信号量并初始化她的值。
参数:
sem:信号量的地址
pshared:如果pshared=0,则信号量在线程之间共享;pshared不等于于0,信号量在进程间共享。
value:信号量的初始值;
返回值
成功完成后,sem_init()函数将在sem中初始化信号量并返回0。
否则,它将返回-1并,表示错误。

信号的p操作
int sem_wait(sem_t *sem);
功能:
通过对sem引用的信号量执行信号量锁操作来锁定信号量。如果信号量值当前为0,那么调用线程将不会从调用返回到sem_wait(),直到锁定信号量或调用被信号中断。
参数:
sem:信号量的地址
返回值:
成功返回0,否则返回 -1;

信号量V操作
int sem_post(sem_t *sem);
功能:
通过对sem引用的信号量执行信号量解锁操作来解锁该信号量。将信号量的值加1,并发出唤醒解锁该信号量。
参数
sem:信号量的地址
返回值:
如果成功,sem_post()函数将返回0;
否则,函数将返回-1,表示错误。

信号量实现互斥

#include <stdio.h> 
#include <pthread.h> 
#include <unistd.h> 
#include <semaphore.h>

sem_t sem;
void display(char *pstr)
{
	sem_wait(&sem); //上锁
	if (pstr == NULL)
		return ;
	while (*pstr != '\0')
	{
		putchar(*pstr); //每次只处理一个字符
		fflush(stdout);//往屏幕中打印字符
		pstr ++;

		sleep(1);
	}
	sem_post(&sem);//解锁

}

void *thread_task1(void *argv)
{
	char *pstr = "abcdef";
	display(pstr);
}

void *thread_task2(void *argv)
{
	char *pstr = "ghijkl";
	display(pstr);
}

int main(int argc, char const *argv[])
{

	pthread_t tid1,tid2;
	if (sem_init(&sem,0,1) != 0)  //0表示是线程,1表示初始化sem值为1
	{
		perror("sem_init");
		return 0;
	}
	//线程创建
	if (pthread_create(&tid1,NULL,thread_task1,NULL) != 0)
	{
		perror("pthread_create");
		return 0;
	} 
	if (pthread_create(&tid2,NULL,thread_task2,NULL) != 0)
	{
		perror("pthread_create");
		return 0;
	} 

	//线程等待
	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
	printf("\nDone\n");
	return 0;
}

运行结果
Linux 进程间通信方式(管道、命名管道、消息队列、信号量、共享内存、套接字)
编译时候用到了线程 所以要加上 -lpthread,结果为什么不是abcdefghijk呢,这得看cpu调度算法;

信号量实现同步

#include <stdio.h> 
#include <pthread.h> 
#include <unistd.h> 
#include <semaphore.h>

sem_t sem1,sem2;
char ch = 'a';


void *pthread_task1(void *argv)
{
	while (1)
	{
		sem_wait(&sem1);
		ch ++;
		sleep(1);
		sem_post(&sem2);
	}
}

void *pthread_task2(void *argv)
{
	while (1)
	{
		sem_wait(&sem2);
		putchar(ch);
		fflush(stdout);
		sem_post(&sem1);
	}
}

int main(int argc, char const *argv[])
{

	pthread_t tid1,tid2;
	if (sem_init(&sem1,0,0) != 0)
	{
		perror("sem_init");
		return 0;
	}

	if (sem_init(&sem2,0,1) != 0)
	{
		perror("sem_init");
		return 0;
	}

	if (pthread_create(&tid1,NULL,pthread_task1,NULL) !=0 )
	{
		perror("pthread_create");
		return 0;
	}

	if (pthread_create(&tid2,NULL,pthread_task2,NULL) !=0 )
	{
		perror("pthread_create");
		return 0;
	}

	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
	printf("Done\n");
	
	return 0;
}

输出结果
Linux 进程间通信方式(管道、命名管道、消息队列、信号量、共享内存、套接字)
验证了信号量实现同步的功能。

共享内存
共享内存运行多个进程共享给定的存储区域。共享内存是进程间共享数据最快的方法。使用共享内存需要注意的是多个进程之间的对一个给定的存储区访问的互斥,需要用到前面所用到的信号量来实现同步与互斥;

函数原型
共享存储的标识符
int shmget(key_t key, size_t size, int shmflg);
功能:
创建或打开一块共享内存区
参数:
key:IPC键值
size:共享内存段的长度
shmflg:标识函数的共享内存权限
IPC_CREAT:如果不存在就创建 ;
IPC_EXCL:如果已经存在则返回失败;

返回值
成功完成后,shmget()返回一个非负整数,即共享内存标识符;
否则,它将返回-1,表示错误。

共享内存映射
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:
将一个共享内存段映射到调用进程的数据段中。
将与shmid指定的共享内存标识符相关联的共享内存段附加到调用进程的地址空间。
如果shmaddr是一个空指针,则在系统选择的第一个可用地址处附加段。如果shmaddr不是一个空指针,并且(shmflg &SHM_RND)为0,则在shmaddr给出的地址处附加段。

参数:
shmid:共享内存的标识符
shmaddr:共享内存映射地址,一般使用NULL。
shmflg:共享内存段的访问权限和映射条件。
如果(shmflg &SHM_RDONLY)非零且调用进程具有读权限,则附加该段进行读取;否则,如果它是0,并且调用进程具有读和写权限,则附加该段用于读和写。

返回值
成功返回0,失败返回 -1;

解除共享内存映射
int shmdt(const void *shmaddr);
功能:
从调用进程的地址空间中分离位于shmaddr指定地址的共享内存段。
参数:
shmaddr:共享内存映射地址。
返回值:
成功返回0;
否则,共享内存段将不会被分离,shmdt()将返回-1,为指示错误。

共享内存控制
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:
共享内存空间的控制
参数:
shmid:共享内存的标识符
cmd:函数功能的控制。
IPC_RMID:删除。
IPC_SET:设置 shmid_ds 参数。
IPC_STAT:保存 shmid_ds 参数。
SHM_LOCK:锁定共享内存段(超级用户)。
SHM_UNLOCK:解锁共享内存段。
buf: shmid_ds 数据类型的地址,用来存放或修改共享内存的属性。

返回值
成功返回0,失败返回-1

share_write.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SIZE 1024*2

int main(int argc, char *argv[])
{
	int shmid;
	int ret;
	key_t key;
	char *shmadd;
	
	if ((key= ftok(".", 2012)) == -1)
	{
		perror("ftok");
	}
	system("ipcs -m");//查看共享内存
	

	if ((shmid = shmget(key, SIZE, IPC_CREAT|0666)) < 0) 
	{ 
		perror("shmget"); 
		exit(-1); 
	} 
	
	if ((shmadd = shmat(shmid, NULL, 0)) < 0)
	{
		perror("shmat");
		exit(-1);
	}
	
	printf("data =%s\n", shmadd);
	if ((ret = shmdt(shmadd)) < 0)
	{
		perror("shmdt");
		exit(1);
	}
	else
	{
		printf("deleted shared memory\n");
	}
	shmctl(shmid, IPC_RMID, NULL);
	system("ipcs -m");
	return 0;
}

share_read.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SIZE 1024*2

int main(int argc, char *argv[])
{
	int shmid;
	int ret;
	key_t key;
	char *shmadd;
	
	if((key = ftok(".", 2012)) == -1)
	{
		perror("ftok");
	}
	
	if ((shmid = shmget(key, SIZE, IPC_CREAT|0666)) < 0) //创建共享内存
	{ 
		perror("shmget"); 
		exit(-1); 
	} 
	
	if((shmadd = shmat(shmid, NULL, 0)) < 0)//映射
	{
		perror("shmat");
		_exit(-1);
	}
	
	printf("copy data to shared-memory\n");//拷贝数据至共享内存区
	bzero(shmadd, SIZE);
	strcpy(shmadd, "data in shared memory\n");
	return 0;
}


输出结果

Linux 进程间通信方式(管道、命名管道、消息队列、信号量、共享内存、套接字)

套接字
套接字是一种通信机制,凭借这种机制,不同主机之间的进程可以进行通信。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。

套接字的特性
1.域
2.类型
3.协议

套接字的域
域指定套接字通信中使用的网络介质。最常见的套接字域是 AF_INET(IPv4)或者AF_INET6(IPV6);

套接字的类型
流套接字(SOCK_STREAM):
流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP协议。

数据报套接字(SOCK_DGRAM):
数据报套接字提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP协议进行数据的传输。

原始套接字(SOCK_RAW):
原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。

总结
本文简单介绍了进程间通信的常见方式,除了套接字没有例子,剩下的都有简单的例子。套接字(socket)应该目前应用最广泛的进程间通信方式。能用于不同计算机之间的不同进程间通信。套接字话题太大,等以后写网络编程的时候,再探讨这个话题;

上一篇:System V 信号量


下一篇:进程间通信—信号量