接着上一篇继续介绍进程间的通信

4、信号量

(1)信号量概述

在多任务操作系统环境下,多进程/线程会同时运行。多个任务可能会为了完成同一个目标而相互协作,这样新形成任务之间的同步关系。同样,在不同人物之间为了争夺有限的系统资源(硬件或软件资源)会进入竞争状态,这就是任务之间的互斥关系。任务之间的互斥与同步关系存在的根源在于临界资源。临界资源是指在同一时刻只允许有限个(通常只有一个)任务可以访问(读)或修改(写)的资源,通常包括硬件资源(处理器、内存、存储器以及其他外围设备)和软件资源(共享代码段、共享结构和变量)。访问临界资源的代码成为临界区。

信号量是用来解决进程/线程之间的同步与互斥问题的一种通信机制,包括一个称为信号量的变量和在该信号下等到资源的进程等待队列,以及对信号量进行的两个原子操作(PV操作)。其中信号量对应于某一种资源,取一个非负的整型值。信号量值指的是当前可用的该资源的数量,若它等于0则意味着目前没有可用的资源。

PV原子操作定义如下:

  • P操作:如果有可用资源(信号量值大于0),则占用一个资源(信号量值减1,进入临界区代码);如果没有可用的资源(信号量值为0),则被阻塞,直到系统资源分配给该任务(进入等待队列,一直等到资源时被唤醒)
  • V操作:如果在该信号量的等待队列中有任务在等待资源,则唤醒一个阻塞任务。如果没有任务等待它,则释放一个资源(信号量值加1)

(2)信号量编程

在linux系统中,使用信号量通常分为一下几个步骤

①  创建信号量或获得系统已经存在的信号量,此时需要调用semget()函数。不同进程通过使用同一个信号量键值来获得同一个信号量

②  初始化信号量,此时使用semctl()函数的SETCVAL操作。当使用二维信号量时,通常将信号量初始化为1

③  进行信号量的PV操作,此时调用semop()函数。这一步是实现进程之间的同步和互斥的核心工作部分

④  如果不需要信号量,则从系统中删除它,稀释使用semclt()函数的IPC_RMID操作。此时需要注意,在程序中不应该出现对已经被删除的信号量的操作

semget()函数语法要点

所需头文件#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型int semget(key_t key, int nsems, int semflg);
函数传入值key:信号量的键值,多个进程可以同通过它访问同一个信号量。其中有个特殊值IPC_PRIVATE,它用于创建带当前进程的私有信号量
nsems:需要创建的信号量数目,通常取值为1

semflg:同open()函数的权限位,也可以用八进制表示法,其中使用IPC_CREAT标志创建新的信号量,即使该信号已经存在(具有同一个键值的信号量在系统中存在),也不会出错。如果同时使用IPC_EXCL标志可以创建一个新的唯一的信号量,此时如果信号量已经存在,则函数会返回出错

函数返回值成功:信号量标识符,在信号量的其他函数中都会使用该值
出错:-1

semctl()函数的语法要点

所需头文件#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型int semctl(int semid, int semnum, int cmd, ...);
函数传入值semid:semget()函数返回的信号量标识符
semnum:信号量编号,当使用信号量集时才会被用到。通常取值为0,就是使用单个信号量(也是第一个信号量)

cmd:指定对信号量的各种操作,当使用单个信号量时(不是信号量集),常用的操作有一下几种:

IPC_STAT:获得该信号量(或者信号量集合)的semid_ds结构,并存放在由第四个参数arg结构变量的buf域指向的semid_ds结构中。semid_ds是在系统中描述信号量的数据结构

IPC_SETVAL:将信号量设置为arg的val值

IPC_GETVAL:返回信号量的当前值

IPC_RMID:从系统中,删除信号量(或者信号量集)

...:是union semun结构,可能在某些系统中不给出该结构的定义,此时必须由程序员自己定义

函数返回值

成功:根据cmd的不同,返回值也不同

IPC_STAT、IPC_SETVAL、IPC_RMID:返回0

IPC_GETVAL:返回信号量的当前值

出错:-1

semop()函数的语法要点

所需头文件#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型int semop(int semid, struct sembuf *sops, unsigned nsops);
函数传入值semid:semget()函数返回的信号量标识符

sops:指向信号量操作数组,一个数组包括一下成员

struct sembuf

{

short sem_num;    //信号量编号,使用单个信号量时,通常取值位0

short sem_op;    //信号量操作:取值为-1,表示P操作。取值为+1表示V操作

short sem_flg;    //通常设置为SEM_UNDO,这样在进程没释放信号量而退出时,系统自动释放该进程中未释放的信号量

}

nsops:操作数组sops中的操作个数(元素数目),通常取值为1(一个操作)
函数返回值成功:信号量标识符,在信号量的其他函数中都会使用该值
出错:-1

(3)实例演示

下面的实例解决了多进程之间的同步问题。

因为信号量相关的函数调用接口比较复杂,所以将他们封装成几个基本函数。

/*sem_com.c*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define DELAY_TIME 3

union semun
{
	int val;
	struct semid_ds *buf;
	unsigned short *array;
};

/*信号量初始化(赋值)函数*/
int init_sem(int sem_id, int init_value)
{
	union semun sem_union;
	sem_union.val = init_value;		/*init_value为初始值*/
	if((semctl(sem_id, 0, SETVAL, sem_union)) == -1)
	{
		perror("Initialize semaphore");
		exit(-1);
	}
	return 0;
}

/*从系统中删除信号量的函数*/
int del_sem(int sem_id)
{
	union semun sem_union;
	if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
	{
		perror("Delete semaphore");
		exit(-1);
	}
}

/*P函数操作*/
int sem_p(int sem_id)
{
	struct sembuf sem_b;
	sem_b.sem_num = 0;			/*单个信号量的编号应该为0*/
	sem_b.sem_op = -1;			/*表示P操作*/
	sem_b.sem_flg = SEM_UNDO;	/*系统自动释放在系统中残留的信号量*/
	if(semop(sem_id, &sem_b, 1) == -1)
	{
		perror("P operation");
		exit(-1);
	}
	return 0;
}

/*P函数操作*/
int sem_v(int sem_id)
{
	struct sembuf sem_b;
	sem_b.sem_num = 0;			/*单个信号量的编号应该为0*/
	sem_b.sem_op = 1;           /*表示V操作*/
	sem_b.sem_flg = SEM_UNDO;   /*系统自动释放在系统中残留的信号量*/
	if(semop(sem_id, &sem_b, 1) == -1)
	{
		perror("V operation");
		exit(-1);
	}
	return 0;
}


int main(int argc, char *argv[])
{
	pid_t pid;
	int sem_id;
	int key;
/*	
	key = ftok("/opt", 0x66 );
	if (key < 0)
	{
		perror("ftok key error");
	 	exit(-1);
	}
*/
	sem_id = semget(ftok(".", 'a'), 1, 0666|IPC_CREAT);	/*创建一个信号量*/
	init_sem(sem_id, 0);
	
	/*创建子进程*/
	if((pid = fork()) < 0)
	{
		perror("fork error");
		exit(-1);
	}
	else if(pid == 0)
	{
		printf("Child process will wait for some seconds...\n");
		sleep(DELAY_TIME);
		printf("The returnd value is %d in the child process(PID = %d)\n", pid, getpid());
		sem_v(sem_id);
	}
	else
	{
		sem_p(sem_id);
		printf("The return value is %d in the father process(PID = %d)\n", pid, getpid());
		sem_v(sem_id);
		del_sem(sem_id);
	}
	exit(0);
}

运行结果如下:

5、共享内存

(1)共享内存概述

可以说,共享内存是一种最为高效的进程间通信方式。因为进程可以直接读写内存,不需要任何数据的复制。为了在多个进程间交换信息,内核专门留出一块内存区。这段内存区可以由需要访问的进程将其映射到自己的私有地址空间。因此进程就可以直接读写这一块内存而不需要进行数据的复制,从而大大提高了效率。当然由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量。

(2)共享内存编程

共享内存的实现分为两个步骤:

  • 第一步是创建共享内存,这里用到的函数是shnget(),也就是从内存中获得一段共享内存区域
  • 第二步是映射共享内存,也就是把这段创建的共享内存映射到具体的进程空间中,这里使用的函数是shmat()。

到这里就可以使用共享内存了,也就是可以使用不带缓冲的I/O读写命令对其进行操作。当然还有撤销映射的操作,其函数为shmdt()

shmget()函数的语法要点

所需头文件

#include <sys/ipc.h>
#include <sys/shm.h>

#include<sys/types.h>

函数原型int shmget(key_t key, size_t size, int shmflg);
函数传入值key:共享内存的键值,多个进程可以通过它访问同一个共享内存。其中有个特殊的值IPC_PRIVATE,它用于创建当前进程的私有共享内存
size:共享内存区大小
shmflg:同open()函数的权限位,也可以用八进制表示法
函数返回值成功:返回共享内存段标识符
出错:-1

shemat()函数的语法要点

所需头文件

#include <sys/ipc.h>
#include <sys/shm.h>

#include<sys/types.h>

函数原型void *shmat(int shmid, const void *shmaddr, int shmflg);
函数传入值shmid:要映射的共享内存区标识符
shmaddr:将共享内存映射到指定地址(若为0则表示系统自动分配地址并把该段共享内存映射到调用进程的地址空间)
shmflgSHM_RDONLY:只读共享内存
默认0:共享内存可读写
函数返回值成功:被映射的段地址
出错:-1

shemdt()函数的语法要点

所需头文件

#include <sys/ipc.h>
#include <sys/shm.h>

#include<sys/types.h>

函数原型int shmdt(const void *shmaddr);
函数传入值shmaddr:被映射的共享内存段地址
函数返回值成功:0
出错:-1

(3)实例演示

/*shme.c*/

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

#include<sys/types.h>

#define BUF_SIZE 2048

int main(int argc, char *argv[])
{
	pid_t pid;
	int shmid;
	char *shm_addr;
	char flag[] = "WROTE";
	char buf[BUF_SIZE];
	
	/*创建共享内存*/
	if((shmid = shmget(IPC_PRIVATE, BUF_SIZE, 0666)) == -1)
	{
		perror("shmget");
		exit(-1);
	}
	else
	{
		printf("Create shared-memory: %d\n", shmid);
	}
	
	/*显示共享内存情况*/
	system("ipcs -m");
	if((pid = fork()) < 0)
	{
		perror("fork");
		exit(-1);
	}
	
	else if(pid == 0)
	{
		/*映射共享内存*/
		shm_addr = (char*)shmat(shmid, NULL, 0);
		if((int)shm_addr == -1)
		{
			printf("Child;shmat");
			exit(-1);
		}
		else
		{
			printf("Child: Attach shared-memory: %p\n", shm_addr);
		}
		system("ipcs -m");
		/*通过检查共享内存的头部是否有标志字符串WROTE来确认父进程已经向共享内存写入有效数据*/
		/*若str1与str2的前n个字符相同,则返回0;若s1大于s2,则返回大于0的值;若s1 小于s2,则返回小于0的值*/
		while(strncmp(shm_addr, flag, strlen(flag)))
		{
			printf("Child: Wait for enable data...\n");
			sleep(5);
		}
		/*获取有效内存的有效数据并显示*/
		strcpy(buf, shm_addr+strlen(flag));
		printf("Child: Shared-memory: %s\n", buf);
		
		/*解除共享内存映射*/
		if((shmdt(shm_addr)) == -1)
		{
			perror("shmdt");
			exit(-1);
		}
		else
		{
			printf("Child Deatach shared-memory\n");
		}
		system("ipcs -m");
		/*删除共享内存*/
		if((shmctl(shmid, IPC_RMID, NULL)) == -1)
		{
			perror("Child: shmctl(IPC_RMID)");
			exit(-1);
		}
		else
		{
			printf("Delete shared-memory success\n");
		}
		system("ipcs -m");
	}
	
	/*父进程处理*/
	else
	{
		if((shm_addr = shmat(shmid, 0, 0)) == (void*)-1)
		{
			perror("shmat");
			exit(-1);
		}
		else
		{
//			printf("Parent: Attach shared-memory: %p\n". shm_addr);
		}
		sleep(1);
		printf("\nInput some string:\n");
		fgets(buf, BUF_SIZE, stdin);
		
		/*在输入的数据添加标志字符串WROTE,来实现父子进程之间的同步*/
		strncpy(shm_addr+strlen(flag), buf, strlen(buf));
		strncpy(shm_addr, flag, strlen(flag));
		
		/*解除共享内存映射*/
		if((shmdt(shm_addr)) == -1)
		{
			perror("Parent shmdt");
			exit(-1);
		}
		else
		{
			printf("Parent: Deatach shared-memory\n");
		}
		system("ipcs -m");
		waitpid(pid,  NULL, 0);
		printf("Finishde\n");
	}
	exit(0);
}

(4)因为是在父子进程之间共享内存所以使用的键值是IPC_PRIVATE。程序的运行过程就是:父进程先等待用户输入,然后用户输入字符串写入共享内存,之后往共享内存的头部写入“WROTE”字符串表示覅进程已经成功写入数据。子进程一直等到共享内存的头部字符串为“WROTE”,然后将共享内存的有效数据(在父进程中输入的字符串)在屏幕上打印出来。父子进程在完成工作后,分别解除与共享内存的映射关系。最后在子进程中删除共享内存。

因为在共享内存本身不提供同步机制,所以应该额外使用不同进程之间的同步(信号量)。在本程序中是使用了简单的标志性字符串实现的简单的父子进程之间的同步。

(5)用共享内存实现没有亲缘关系的进程之间的通信

/*shm_write.c
创建共享内存,并写入数据*/
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>

typedef struct
{
    char name[8];
    int age;
} people;

int main(int argc, char** argv)
{
    int shm_id,i;
    key_t key;
    char temp[8];
    people *p_map;
    char pathname[30] ;

    strcpy(pathname,"/mydir");
    key = ftok(pathname,0x03);
    if(key==-1)
    {
        perror("ftok error");
        return -1;
    }
    printf("key=%d\n",key) ;
    shm_id=shmget(key,4096,IPC_CREAT|IPC_EXCL|0600); 
    if(shm_id==-1)
    {
        perror("shmget error");
        return -1;
    }
    printf("shm_id=%d\n", shm_id) ;
    p_map=(people*)shmat(shm_id,NULL,0);
    memset(temp, 0x00, sizeof(temp));
    strcpy(temp,"test") ;
    temp[4]='0';
    for(i = 0;i<3;i++)
    {
        temp[4]+=1;
        strncpy((p_map+i)->name,temp,5);
        (p_map+i)->age=0+i;
    }
    shmdt(p_map) ;
    return 0 ;
}
/*shm_read.c
实现共享内存的读*/
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct
{
    char name[8];
    int age;
} people;
int main(int argc, char** argv)
{
    int shm_id,i;
    key_t key;
    people *p_map;
    char pathname[30] ;
 
    strcpy(pathname,"/mydir");
    key = ftok(pathname,0x03);
    if(key == -1)
    {
        perror("ftok error");
        return -1;
    }
    printf("key=%d\n", key) ;
    shm_id = shmget(key,0, 0);   
    if(shm_id == -1)
    {
        perror("shmget error");
        return -1;
    }
    printf("shm_id=%d\n", shm_id);
    p_map = (people*)shmat(shm_id,NULL,0);
    for(i = 0;i<3;i++)
    {
        printf( "name:%s\n",(*(p_map+i)).name );
        printf( "age %d\n",(*(p_map+i)).age );
    }
    if(shmdt(p_map) == -1)
    {
        perror("detach error");
        return -1;
    }
    return 0 ;
}

参考:https://blog.csdn.net/guoping16/article/details/6584058

6、消息队列

(1)消息队列概述

消息队列就是一些消息的列表。用户可以在消息队列中添加消息或者读取消息等。从这点来看,消息队列具有一定的FIFO的特性,但是他可以实现消息的随即查询,比FIFO具有更大的优势。同时这些消息又存在于内核中,由“队列ID”来标识。

(2)消息队列编程

队列消息的实现包括创建或打打开消息队列、添加消息队列、读取消息队列和控制消息队列这四种操作。其中创建或打开消息队列使用的函数是msgget(),这里创建的消息队列的数量会系统消息队列数量的限制;添加消息队列的函数是msgsnd(),它把消息添加到以打开的消息队列末尾;读取消息的函数是msgrcv(),它把消息从消息队列中取走,与FIFO不同的是,这里可以取走指定的某一条消息;控制消息队列的函数是msgctl()。它可以完成多项功能

msgget()函数语法要点

所需头文件#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型int msgget(key_t key, int msgflg);
函数传入值key:消息队列的键值,多个进程可以通过它访问同一个消息队列,其中有个特殊的值IPC_PRIVATE。它用于创建当前进程私有的消息队列
msgflg:权限标志位
函数返回值成功:消息队列ID
错误:-1

msgsnd()函数语法要点

所需头文件#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
函数传入值msqid:消息队列的队列ID

msgp:指向消息结构的指针。该消息结构msgbuf通常为:

struct msgbuf

{

long mtype;  //消息类型,该结构必须从这个域开始

char metxt[1];  //消息正文

}

msgsz:消息正文的字节数(不包括消息类型指针变量)
msgflg:IPC_NOWAIT若消息无法立即发送(比如:消息队列已满),函数会立即返回
0:msgsnd调用阻塞直到发送成功为止
函数返回值成功:0
出错:-1

msgrcv()函数语法要点

所需头文件#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
函数传入值msqid:消息队列的队列ID
msgp:消息缓冲区,同于msgsnd()函数的msgp
msgsz:消息正文的字节数(不包括消息类型指针变量)
msgtyp0:接收消息队列中第一个消息
大于0:接收消息队列中第一个类型为msgtyp的消息
小于0:接收消息队列中第一个类型值不小于msgtyp绝对值且雷西你个值又小的消息
msgflgMSG_NOERROR:若返回消息比msgsz字节多,则消息就会截短到msgsz字节,且不通知消息发送进程
IPC_NOWAIT:若消息队列中没有相应类型的消息可以接收,则函数立即返回
0:msgsnd()调用阻塞直到接收一条相应类型的消息为止
函数返回值成功:0
出错:-1

msgctl()函数语法要点

所需头文件#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型int msgctl(int msqid, int cmd, struct msqid_ds *buf);
函数传入值msqid:消息队列的队列ID
cmdIPC_STAT:读取消息队列的数据结构msqid_ds,并将其存储在buf指定的地址中
IPC_SET:设置消息队列的数据结构msqid_ds中的ipc_perm域(IPC操作权限描述结构)值,这个值取自buf参数
IPC_RMID:从系统内核中删除消息队列
buf:描述消息队列的msqid_ds结构类型变量
/
函数返回值成功:0
出错:-1

(3)实例代码演示

/*msgsnd.c*/


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

#define BUF_SIZE 512

struct message
{
	long msg_type;
	char msg_text[BUF_SIZE];
};

int main (int argc, char *argv[])
{
	key_t key;
	int qid;
	struct message msg;
	
	/*根据不同的路径和关键字产生标准的key*/
	if((key = ftok(".", 'a')) == -1)
	{
		perror("ftok error");
		exit(1);
	}
	/*创建消息队列*/
	if((qid = msgget(key, IPC_CREAT|0666)) == -1)
	{
		perror("msgget error");
		exit(1);
	}
	printf("Open queue %d\n", qid);
	while(1)
	{
		printf("Enter some message to the queue: ");
		if((fgets(msg.msg_text, BUF_SIZE, stdin)) == NULL)
		{
			puts("no message");
			exit(1);
		}
		msg.msg_type = getpid();
		/*添加消息到消息队列*/
		if((msgsnd(qid, &msg, strlen(msg.msg_text), 0)) < 0)
		{
			perror("msgsnd error");
			exit(1);
		}
		if(strncmp(msg.msg_text, "quit", 4) == 0)
		{
			break;
		}
	}
	exit(0);
}
/*msgsnd.c*/


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

#define BUF_SIZE 512

struct message
{
	long msg_type;
	char msg_text[BUF_SIZE];
};

int main (int argc, char *argv[])
{
	key_t key;
	int qid;
	struct message msg;
	
	/*根据不同的路径和关键字产生标准的key*/
	if((key = ftok(".", 'a')) == -1)
	{
		perror("ftok error");
		exit(1);
	}
	/*创建消息队列*/
	if((qid = msgget(key, IPC_CREAT|0666)) == -1)
	{
		perror("msgget error");
		exit(1);
	}
	printf("Open queue %d\n", qid);
	do
	{
		memset(msg.msg_text, 0, BUF_SIZE);
		if((msgrcv(qid, (void*)&msg, BUF_SIZE, 0, 0)) < 0)
		{
			perror("msgrcv error");
			exit(1);
		}
		printf("The message from process %d: %s", (int)msg.msg_type, msg.msg_text);
	} while(strncmp(msg.msg_text, "quit", 4));
	
	/*从内系统核中移走消息队列*/
	if((msgctl(qid, IPC_RMID, NULL)) < 0)
	{
		perror("msgctl error");
		exit(1);
	}
	
	exit(0);
}

 

 

至此,linux中的进程间通信机制已经写完了,如有不对的地方,望大家指出。

linux进程间通信(上):https://blog.csdn.net/David_361/article/details/86572307

 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐