unix系统之进程间通信

本文深入探讨了进程间通信的几种方法,包括管道(半双工,适用于父子进程)、命名管道(FIFO,允许无亲缘关系进程通信)、消息队列(存储在内核,可随机查询、按类型读取)、共享内存(快速通信,配合信号量使用)和信号量(用于同步和保护资源访问)。通过示例代码展示了如何创建、使用这些通信机制,强调了各自的特点和应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

进程之间交互信息可以通过函数fork、exec或者是文件系统来传送打开的文件,而进程间的相互通信则是由IPC(InterProcess Communication)技术实现。

通信方式

  • 管道:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用,进程的亲缘关系通常是指父子进程关系。
  • 命名管道FIFO:FIFO也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  • 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
  • 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 套接字Socket:套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
  • 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

管道

概述

管道是UNIX系统IPC的最古老的通信方式,所有UNIX系统都提供该通信方式,它是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。
当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。
管道可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

相应函数1

管道是通过调用pipe函数创建的。

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

其中参数fd是一个数组,一条管道有两个文件描述符,在调用pipe函数成功后,会填充fd数组:

  • fd[0]:通过该文件描述符可以从管道中读取数据。
  • fd[1]:通过该文件描述符可以向管道写入数据。

管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道,如下图所示:
在这里插入图片描述

一般情况下,我们并不需要多余的连接,父进程关闭读端,而子进程关闭写端,这样就行成了一个从父进程到子进程的管道,如下图所示:
在这里插入图片描述

例子1

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

int main() 
{
	int n;
	int fd[2];
	pid_t pid;
	char pipe_buf;
	memset(fd, 0, sizeof(fd));
	if(pipe(fd) == -1)
	{
		return -1;
        }
	if((pid = fork()) < 0)
	{
		return -1;
	} 
	// 父进程关读端,写hello
	else if (pid > 0)
	{
		close(fd[0]);
		write(fd[1], "hello", 5);
	}
	// 子进程关写端,读hello并打印
	else
	{
		close(fd[1]);
		n = read(fd[0],&pipe_buf, 5);
		write(STDOUT_FILENO, &pipe_buf, n);
	}
	exit(0);
 
}

相应函数2

标准I/O库为此又提供了2个函数,popen与pclose,来简化创建一个连接到另一个进程的管道:

#include<stdio.h>

// 返回值,成功则返回文件指针,失败返回NULL
FILE *popen(const char *cmdstring, const char *type);
// 返回值, 成功返回cmdstring的终止状态,失败返回-1
int pclose(FILE *fp);
  • popen:通过创建一个管道,调用fork()产生一个子进程,然后调用exec执行cmdstring。type有“r”跟“w”两种值,“r”则文件指针连接到cmdstring的标准输出,“w”则则文件指针连接到cmdstring的标准输入。
  • pclose:关闭标准I/O流,等待命令的终止。

例子2

#include <sys/types.h>  
#include <unistd.h>  
#include <stdlib.h>  
#include <stdio.h>  
#include <string.h>
 
int main()  
{  
    FILE   *fpin;  
    FILE   *fpout;
    char   buf[1024]; 
    // 初始化buf,以免后面写如乱码到文件中
    memset( buf, '\0', sizeof(buf) );
    // 将“ls -l”命令的输出 通过管道读取(“r”参数)到FILE* stream
    fpin = popen( "ls -l", "r" ); 
    // 新建一个可写的文件
    fpout = fopen( "test_popen.txt", "w+"); 
    // 将刚刚FILE* fpin的数据流读取到buf中
    fread( buf, sizeof(char), sizeof(buf),  fpin);
    // 将buf中的数据写到FILE* fpout对应的流中,也是写到文件中  
    fwrite( buf, 1, sizeof(buf), fpout);
    
    pclose( fpin );  
    fclose( fpout );
    
    return 0;
} 

FIFO

概述

和管道的主要区别在于,FIFO在磁盘上有文件 .p文件,但磁盘上的文件大小始终为零,它在内核缓冲区内有一个对应的缓冲区,数据是存储在缓冲区中而不是磁盘中,所以其实FIFO也是一个伪文件,但在磁盘上有文件名,所以能够实现没有血缘关系的进程间通信。

相应函数

创建FIFO类似于创建文件:

#include<sys/stat.h>
// 返回值:成功0,出错-1
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);

FIFO有2个用途:

  • shell命令使用FIFO将数据从一条管道传送到另一条管道时,无需创建中间临时文件。
  • 客户进程-服务器进程关系中,FIFO用作汇聚点,在客户进程和服务器进程之间传递数据。

例子

创建一个读(fifo_read.c)与写(fifo_write.c)的进程,然后通过FIFO进行通信,写进程不断打印hello world,读进程不断输出hello world。

gcc -o fifo_read fifo_read.c
gcc -o fifo_write fifo_wirte.c
./fifo_read fifo1
# 切换到另一个shell窗口
./fifo_write fifo1

演示结果如下:
在这里插入图片描述

fifo_write.c

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<fcntl.h>
 
int main(int argc,const char* argv[])
{
  if(argc<2)
   {
     printf("./a.out fifioname\n");
     exit(1);
   }
   //判断文件是否存在
   int ret =access(argv[1],F_OK);
   if(ret==-1)
   {
     int r=mkfifo(argv[1],0664);
     if(r==-1)
      {
        perror("mkfifo error");
        exit(1);
      }
      printf("创建有名管道%s成功\n",argv[1]);
   }
   int fd=open(argv[1],O_WRONLY);		
   if(fd==-1)
   {
     perror("open error");
     exit(1); 
   }
   char *p="hello,world";
   while(1) 
   {
     sleep(1);
     int len=write(fd,p,strlen(p)+1);
   }
   close(fd);
   return 0;
}


fifo_read.c

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<fcntl.h>
 
int main(int argc,const char* argv[])
{
  if(argc<2)
   {
     printf("./a.out fifioname\n");
     exit(1);
   }
   //判断文件是否存在
   int ret =access(argv[1],F_OK);
   if(ret==-1)
   {
     int r=mkfifo(argv[1],0664);
     if(r==-1)
      {
        perror("mkfifo error");
        exit(1);
      }
      printf("创建有名管道%s成功\n",argv[1]);
   }
   int fd=open(argv[1],O_RDONLY);		
   if(fd==-1)
   {
     perror("open error");
     exit(1); 
   }
  char buf[512];
   while(1) 
   {
     int len=read(fd,buf,sizeof(buf));
     buf[len]=0;
     printf("buf=%s,len=%d\n",buf,len);
   }
   close(fd);
   return 0;
}

消息队列

概述

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

  1. 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
  2. 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
  3. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

相关函数

ftok

ftok() 函数用于获得一个IPC 关键字

#include <sys/types.h>
#include <sys/ipc.h>
 
key_t ftok(const char *pathname, int proj_id);

ftok函数返回一个以pathname 指向的文件相对应的文件和proj_id 来确定一个IPC 关键字key_t。pathname必须是一个已经存在并具有访问权限的文件,proj_id 只有最低的8个字节是有效的,所以通常用一个ASCII 字符来作为proj_id。

当pathname 和proj_id 完全相同时,每次调用ftok() 都会获取一个相同的关键字。

IPC 关键字可以通过ftok() 函数来获得,也可以设为IPC_PRIVATE,这时操作系统确保创建一个新的IPC,其标识符需要由进程自己记录并告诉其他进程。

创建消息队列

系统调用msgget() 来创建一个新的消息队列,或者存取一个已经存在的消息队列,原型:

#include <sys/msg.h>
 
int msgget(key_t key, int msgflag);
  • key:某个消息队列的名字,用ftok()产生
  • msgflag:有两个选项IPC_CREAT和IPC_EXCL,单独使用IPC_CREAT,如果消息队列不存在则创建之,如果存在则打开返回;单独使用IPC_EXCL是没有意义的;两个同时使用,如果消息队列不存在则创建之,如果存在则出错返回。
  • 返回值:成功返回一个非负整数,即消息队列的标识码,失败返回-1。可以通过errno和perror函数来查看错误信息。

控制消息队列

消息队列标识符的属性被记录在一个msgid_ds的结构体:

struct msqid_ds {
    struct ipc_perm msg_perm;        /* structure describing operation permission */
    time_t          msg_stime;       /* time of last msgsnd command */
    time_t          msg_rtime;       /* time of last msgrcv command */
    time_t          msg_ctime;       /* time of last change */
    unsigned log    __msg_cbytes;    /* current number of bytes on queue */
    msgqnum_t       msg_qnum;        /* number of messages currently on queue */
    msglen_t        msg_qbytes;      /* max number of bytes allowed on queue */
    pid_t           msg_lspid;       /* pid of last msgsnd() */
    pid_t           msg_lrpid;       /* pid of last msgrcv() */
    ...
}

通过msgctl() 可以对消息队列进行控制或者一些属性的修改,函数原型:

#include <sys/msg.h>          //里面包含了 #include <sys/ipc.h>
 
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • msqid:由msgget函数返回的消息队列标识码
  • cmd:有三个可选的值
    • IPC_STAT 把msqid_ds结构中的数据设置为消息队列的当前关联值
    • IPC_SET 在进程有足够权限的前提下,把消息队列的当前关联值设置为msqid_ds数据结构中给出的
    • IPC_RMID 删除消息队列
  • 返回值: 成功返回0,失败返回-1

发送消息

msgsnd() 向队列发送一条消息,函数原型:

#include <sys/msg.h>
 
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  • msgid:由msgget函数返回的消息队列标识码
  • msgp:指针指向准备发送的消息
  • msgze:msgp指向的消息的长度(不包括消息类型的long int长整型)
  • msgflg:默认为0 ,即从队列中取出最长时间的一条消息。
  • 返回值:成功返回0,失败返回-1

消息结构一方面必须小于系统规定的上限,另一方面必须以一个long int长整型开始,接受者以此来确定消息的类型。

接受消息

msgrcv() 用于从消息队列中读取一条消息,函数原型:

#include <sys/msg.h>
 
int msgrcv(int msqid, const void *msgp, size_t msgsz, long msgtype int msgflg);
  • msgid:由msgget函数返回的消息队列标识码
  • msgp:指针指向准备发送的消息
  • msgze:msgp指向的消息的长度(不包括消息类型的long int长整型)
  • msgtype:指定读取的消息的type
  • msgflg:默认为0, 即从队列中取出最长时间的一条消息。
  • 返回值:成功返回0,失败返回-1

注意,参数msgflg 取值为:

  • 0:从队列中取出最长时间的一条消息。
  • IPC_NOWAIT:当队列没有消息时,调用会立即返回ENOMSG错误。否则,调用进程会被挂起,知道队列中的一条细细满足msgrcv() 的参数要求。

例子

    在两个终端分别运行msg_snd、msg_rcv,一端输入信息发送,另一端会自动接收信息,实现消息传递。

msg_snd.c

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/msg.h>
#include <errno.h>
#define MAX_TEXT 512
struct msg_st
{  
    long int msg_type; 
    char text[MAX_TEXT];
};
int main()
{  
    int running = 1;   
    struct msg_st data;
    char buffer[BUFSIZ];
    int msgid = -1;
    // 建立消息队列
    msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
    if(msgid == -1)
    {  
        fprintf(stderr, "msgget failed with error: %d\n", errno);  
        exit(EXIT_FAILURE);
    }
    // 向消息队列中写消息,直到写入end
    while(running) 
    {        
        printf("Enter some text: ");   
        fgets(buffer, BUFSIZ, stdin);  
        data.msg_type = 1; 
        strcpy(data.text, buffer);
        //向队列发送数据  
        if(msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1) 
        {      
            fprintf(stderr, "msgsnd failed\n");    
            exit(EXIT_FAILURE);    
        }
        // 输入end结束输入 
        if(strncmp(buffer, "end", 3) == 0)     
            running = 0;   
        sleep(1);
    }  
    exit(EXIT_SUCCESS);
}

msg_rcv.c

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/msg.h>
struct msg_st
{  
    long int msg_type; 
    char text[BUFSIZ];
};
int main()
{  
    int running = 1;   
    int msgid = -1;
    struct msg_st data;
    // 注意1
    long int msgtype = 0;
    // 建立消息队列   
    msgid = msgget((key_t)1234, 0666 | IPC_CREAT); 
    if(msgid == -1)
    {      
        fprintf(stderr, "msgget failed with error: %d\n", errno);  
        exit(EXIT_FAILURE);
    }
    // 从队列中获取消息,直到遇到end消息为止
    while(running) 
    {  
        if(msgrcv(msgid, (void*)&data, BUFSIZ, msgtype, 0) == -1)  
        {  
            fprintf(stderr, "msgrcv failed with errno: %d\n", errno);  
            exit(EXIT_FAILURE);    
        }  
        printf("You wrote: %s\n",data.text);
        // 遇到end结束,跳出循环 
        if(strncmp(data.text, "end", 3) == 0)      
            running = 0;   
    }
    // 删除消息队列
    if(msgctl(msgid, IPC_RMID, 0) == -1)   
    {      
        fprintf(stderr, "msgctl(IPC_RMID) failed\n");  
        exit(EXIT_FAILURE);
    }  
    exit(EXIT_SUCCESS);
}

信号量

概述

信号量与已经介绍过的IPC机构不同,它是一个计数器,用于为多个进程提供对共享数据对象的访问。为了获得共享资源,进程需要执行下列操作:

  • 测试控制该资源的信号量。
  • 若此信号量的值为正,则进程可以使用该资源.。在这种情况下,进程会将信号量值减去1,表示它使用了一个资源单位。
  • 否则,若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0.。如果有进程正在休眠等待此信号量,则唤醒他们。

为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作,为此,信号量通常是在内核中实现的。
信号量有几个需要注意的地方:

  1. 信号量并非是单个非负值,而必需定义为含有一个或多个信号量值的集合。当创建信号量时,要指定集合中信号量值的数量。
  2. 信号量的创建(semget)时独立于它的初始化(semctl)的,这是一个致命的缺点,因为不能原子地创建一个信号量集合,并且对该集合中的各个信号量值赋初值。
  3. 即使没有进程正在使用各种形式的IPC,他们依然是存在的。有的程序在终止时并没有释放已经分配给它的信号量,所以我们不得不为这种程序担心。 后面将要说明undo功能就是处理这种情况的:为了防止多个因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生产并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占试的执行。而信号量就可以提供这样 的一种访问机制,让一个临界区 同一时 间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。其中共享内 存的使用就要用到信号量。

内核为每一个信号量集合维护这一个semid_ds结构:

struct semid_ds
{
	struct ipc_perm sem_perm;
	unsigned short  sem_nsems;
	time_t          sem_otime;
	time_t          sem_ctime;
};

每个信号量由一个无名结构表示,它至少包含下列成员:

struct ...
{
	unsigned short semval;
	pid_t          sempid;
	unsigned 	   semncnt;
	unsigned       semzcnt;
	.
	.
	.
}

最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。

相应函数

创建信号量
#include <sys/sem.h>

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

  • key:信号量的关键字,可以通过ftok() 创建,详细看 进程间通信——消息队列
  • nsems:信号量的数目。如果是创建新的信号量,必须要指定nsems。如果是引用现有的信号量,nsems指定为0。
  • shmflg: 有两个选项,IPC_CREAT 表示内核中没有此信号量则创建它。IPC_EXCL 当和IPC_CREAT一起使用时,如果信号量已经存在,则返回错误。
  • 返回值:成功返回信号量id,失败返回-1。通过errno和perror函数可以查看错误信息。

通过nsems 可以看出,创建一个信号量,里面可能包含多个信号量值。该信号量就相当于一个集合。该值表明有多少个共享资源单位可供共享应用。下面semctl 也是通过nsems 中的某一个进行操作。
如果nsems 设为1,该信号量又被称为二元信号量(binary semaphore)。

控制信号量

信号量标识符信息被记录在结构体semid_ds中:

struct semid_ds {
    struct ipc_perm  sem_perm;       /* operation permission struct */
    time_t           sem_otime;      /* last semop() time */
    time_t           sem_ctime;      /* last time changed by semctl() */
    unsigned short   sem_nsems;      /* number of semaphores in set */
    ...
}
#include <sys/sem.h>
 
int semctl(int semid, int semnum, int cmd, .../* union semun arg */);
  • semid: 信号的标识符
  • semnum: 指定nsems 个信号量中的一个。semnum值在0 和nsems-1 之间,包括0 和 nsems-1。
  • cmd: 有10个命令,其中有5中命令是针对一个特定的信号量值的(semnun 指定的)。
  • arg: 第4个参数,详细见下面分析。
  • 返回值:若成功返回0(GETALL 等GET 命令详细看下面),若出错,则返回-1。通过errno 和perror 函数可以查看错误信息。

第 4 个参数 arg,是否使用取决于所请求的命令,如果使用该参数,则其类型是semun,它是多个命令特定参数的联合,其中包括semid_ds。如下:

union semun {
    int val;                      /* value for cmd SETVAL */
    struct semid_ds *buf;         /* buffer for IPC_STAT & IPC_SET */
    unsigned short int *array;    /* array for GETALL & SETALL */
    struct seminfo *__buf;        /* buffer for IPC_INFO */
}

注意,这个选项参数是一个联合,而非指向联合的指针。

下面来看下cmd 的10 个命令:

  • IPC_STAT 获取semid_ds结构,并存储在由arg.buf 指向的结构中。
  • IPC_SET 按arg.buf 指向的结构中的值,设置与信号量相关的sem_perm.uid、sem_perm.gid 和sem_perm.mode 字段。此命令只能由两种进程执行:一种是其有效用户ID 等于sem_perm.cuid 或sem_perm.uid 的进程;另一种是具有超级用户特权的进程。
  • IPC_RMID 删除信号量。这种删除是立即发生的。删除时仍在使用此信号量的其他进程,在它们下次试图对此信号量进行操作时,将出错返回EIDRM。此命令只能由两种进程执行:一种是其有效用户ID 等于sem_perm.cuid 或sem_perm.uid 的进程;另一种是具有超级用户特权的进程。
  • GETVAL 返回成员semnum 的semval 值
  • SETVAL 设置成员semnum 的semval值,该值由 arg.val 指定。
  • GETPID 返回成员semnum 的sempid 值。
  • GETNCNT 返回成员semnum 的semncnt 值
  • GETZCNT 返回成员semnum 的semzcnt 值
  • GETALL 取出信号量集合中所有信号量的值。这些值存储在 arg.array 指向的数组中。
  • SETALL 将该集合中所有信号量的值设置成arg.array 指向的数组中的值。

对于出GETALL 以外的所有GET 命令,semctl 函数都返回相应值。其他命令,若成功返回0,若出错,返回-1。

信号量PV操作

信号量中最重要的就是P、V操作。P就是减1操作,V就是加1操作。

#include <sys/sem.h>
 
int semop(int semid, struct sembuf semoparray[], size_t nops);
  • semid 信号量的标识符
  • semoparray 一个指针,指向一个由sembuf 结构表示的信号量操作数组。
  • nops 规定semoparray 中操作的数量(元素个数)
  • 返回值:若成功,返回0;若失败,返回-1。通过errno和perror函数可以查看错误信息。

sembuf 的数据结构:

struct sembuf {
    unsigned short int sem_num;          /* semaphore number */
    short int sem_op;                    /* semaphore operation */
    short int sem_flg;                   /* operation flag */
}

sem_op值可以是负值、0 或正值。

  1. 最易于处理的情况是sem_op 为正值。这对应于进程释放的占用的资源数。sem_op 值会加到信号量上。如果指定了undo 标志,则也从该进程的此信号量调整中减去sem_op。
  2. 若sem_op 为负值,则表示要获取由该信号量控制的资源。

如信号量的值大于等于sem_op的绝对值(具有所需的资源),则从信号量值减去sem_op的绝对值。这能保证信号量的结果值大于0。如果指定了undo 标志,则sem_op 的绝对值也加到该进程的此信号量调整值上。

如果信号量值小于sem_op的绝对值(资源不能满足要求),则使用下列条件:

  • 若指定了IPC_NOWAIT,则semop 函数返回EAGAIN。
  • 若未指定IPC_NOWAIT,则该信号量的semncnt值加1(因为调用进程将进入休眠状态),然后调用进程被挂起直至下列事件之一发生:
    • 此信号量值变成大于等于sem_op的绝对值(即某个进程已经释放了某些资源)。此信号量的semncnt值减1(因为已结束等待),并且从信号量值中减去sem_op 的绝对值。如果指定了undo 标志,则sem_op 的绝对值也加到该进程的此信号量调整值上。
    • 从系统中删除了此信号量。在这种情况下,函数出错返回EIDRM。
    • 进程捕捉到一个信号,从此案好处理程序返回,在这种情况下,此信号量的semncnt值减1(因为调用进程不再等待),并且函数出错返回EINTR。
  • 若sem_op 为0,这表示调用进程希望等待到该信号值变0.

如果信号量值当前是0,则此函数立即返回。
如果信号量值非0,则使用下列条件:

  • 若指定了IPC_NOWAIT,则出错返回EAGAIN。
  • 若未指定IPC_NOWAIT,则该信号量的semzcnt值加1(因为调用进程将进入休眠状态),然后调用进程被挂起,直至下面事件发生:
    • 此信号量值变成0。此信号量的semzcnt值减1(因为调用进程已结束等待)。
    • 从系统中删除了此信号量。在这种情况下,函数出错返回EIDRM。
    • 进程捕捉到一个信号,并从信号处理程序返回。在这种情况下,此信号量的semzcnt值减1(因为调用进程不再等待),并且函数出错返回EINTR。

例子

子进程间隔时间输出AA,父进程间隔时间输出BB,如果没有信号量控制的话,输出的AB是无序,比如no_sem.c;如果加入信号量的话,就可以做到输出有序,比如sem.c

no_sem.c

#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<unistd.h>

int main()
{
 
	pid_t id = fork();
	
	if (id == 0)
 	{
		while (1)
		{
			
			printf("A");
			fflush(stdout);
			usleep(100000);
			printf("A ");
			fflush(stdout);
			usleep(300000);
            
		
		}
 
	}
	else
	{
		while (1)
		{
			printf("B");
			fflush(stdout);
			usleep(200000);
			printf("B ");
			fflush(stdout);
			usleep(100000);
		
		}
	}
	return 0;
 
}

sem.c

#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<unistd.h>
#define PATHNAME "."
#define PROJ_ID 0X6666
 
int creatsem(int nums);
int initsem(int semid,int nums,int initval);
int getsem(int nums);
int p(int semid,int who);
int v(int semid,int who);
int destroysem(int semid);

union semun
{
	int val;
};
int commsem(int nums, int flags)
{
	key_t key = ftok(PATHNAME, PROJ_ID);
	if (key<0)
	{
		perror("ftok");
		return -1;
	}
	int semid = semget(key, nums, flags);
	if (semid<0)
	{
		perror("semget");
		return -2;
	}
	return semid;
}
 
int creatsem(int nums)
{
	return commsem(nums, IPC_CREAT | IPC_EXCL | 0666);
}
 
int initsem(int semid, int nums, int value)
{
	union semun u;
	u.val = value;
	if (semctl(semid, nums, SETVAL, u)<0)
	{
		perror("semctl");
		return -1;
	}
	return 0;
}
 
int getsem(int nums)
{
	return commsem(nums, IPC_CREAT);
}
 
int commpv(int semid, int who, int op)
{
	struct sembuf sf;
	sf.sem_num = who;
	sf.sem_op = op;
	sf.sem_flg = 0;
	if (semop(semid, &sf, 1)<0)
	{
		perror("semop");
		return -1;
	}
	return 0;
}
 
int p(int semid, int who)
{
	return commpv(semid, who, -1);
}
 
int v(int semid, int who)
{
	return commpv(semid, who, 1);
}
 
int destroyaem(int semid)
{
	if (semctl(semid, 0, IPC_RMID)<0)
	{
		perror("semctl");
		return -1;
	}
}
int main()
{
 
	int semid = creatsem(1);
	initsem(semid, 0, 1);
	pid_t id = fork();
	int _semid = getsem(0);
	if (id == 0)
 	{
		while (1)
		{
			// 子进程p-> +1
            p(_semid, 0);
			printf("A");
			fflush(stdout);
			usleep(100000);
			printf("A ");
			fflush(stdout);
			usleep(300000);
            // 子进程v-> -1
            v(_semid, 0);
		
		}
 
	}
	else
	{
		while (1)
		{
			// 父进程p-> +1
            p(_semid, 0);
			printf("B");
			fflush(stdout);
			usleep(200000);
			printf("B ");
			fflush(stdout);
			usleep(100000);
            // 父进程v-> -1
            v(_semid, 0);
		
        }
	}
	destroysem(semid);
	return 0;
 
}

共享存储

概述

共享存储允许两个或多个进程共享一个给定的存储区,是进程间通信最快的一种方式。
不要同时对共享存储空间进行写操作,通常,信号量用于同步共享存储访问。

相应函数

创建
#include <sys/shm.h>
 
int shmget(key_t key, size_t size, int shmflg);
  • key 共享存储的关键字,可以通过ftok() 创建,详细看上面的消息队列的相应函数
  • size 共享存储的大小
  • shmflg 有两个选项,IPC_CREAT 表示内核中没有此共享内存则创建它。IPC_EXCL 当和IPC_CREAT一起使用时,如果共享存储已经存在,则返回错误。
  • 返回值:成功返回标识符,否则返回-1。通过errno和perror函数可以查看错误信息。

连接

获取一个共享存储的地址,并将其连接到进程中。

#include <sys/shm.h>
 
void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmid 共享存储的标识符。
  • shmaddr 指定的连接地址。若shmaddr取NULL,表示核心自动选择一个地址;若不为NULL且shmflg⽆SHM_RND标记,则以shmaddr为连接地址;若不为NULL且shmflg设置了SHM_RND标记,则连接的地址会⾃自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
  • shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY(只读共享存储)
  • 返回值:成功返回一个指针,指向共享存储的第⼀个节;失败返回-1
脱离共享存储

当一个进程不在需要共享存储时,用shmdt() 把共享存储从进程地址空间中脱离,但不等于将共享存储段从系统内核中删除。

#include <sys/shm.h>
 
int shmdt(const void *shmaddr);
  • shmaddr 为shmat() 函数的连接共享存储的地址。
  • 返回值:成功返回0;失败返回-1

控制共享存储

在共享存储中标识符的属性被记录在一个shmid_ds的结构中:

struct shmid_ds{
    struct ipc_perm   shm_perm;         /* operation permission struct */
    size_t            shm_segsz;        /* size of segment in bytes */
    time_t            shm_amime;        /* time of last shmat() */
    time_t            shm_dtime;        /* time of last shmdt() */
    time_t            shm_ctime;        /* time of last change by shmctl() */
    pid_t             shm_cpid;         /* pid of creator */
    pid_t             shm_lpid;         /* pid of last shmdt */
    shmatt_t          shm_nattach;      /* number of current attaches */
    ...
}

可以对共享存储进行控制或一些属性的修改:

#include <sys/shm.h>
 
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • shmid:共享存储的标识码
  • cmd:有三个可选的值,在此我们使用IPC_RMID
    • IPC_STAT 把msqid_ds结构中的数据设置为共享内存的当前关联值
    • IPC_SET 进程有足够权限的前提下,把共享内存的当前关联值设置为msqid_ds数据结构中给出的值
    • IPC_RMID 删除共享内存段
  • 返回值:成功返回0;失败返回-1

例子

两个进程,shm_read进程读取共享存储的数据;shm_write进程写共享存储的数据,输入 end 结束进程。
shm_read.c

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

#define SHARE_BUF 16 * 1024
#define TEXT_SZ 2048

struct shared_use_st
{
    int written_by_you;
    char some_text[TEXT_SZ];
};

int main()
{
    int running = 1;
    void *shared_memory = (void *)0;
    struct shared_use_st *shared_stuff;
    int shmid;

    // 获取共享存储
    shmid = shmget((key_t)1, SHARE_BUF, 0666 | IPC_CREAT);
    if (shmid == -1)
    {
        fprintf(stderr, "shmget failed\n");
        exit(-1);
    }

    // 连接共享存储
    shared_memory = shmat(shmid, (void *)0, 0);
    if (shared_memory == (void *)-1)
    {
        fprintf(stderr, "shmat failed\n");
        exit(-1);
    }

    // 转换
    shared_stuff = (struct shared_use_st *)shared_memory;
    shared_stuff->written_by_you = 0;
    while (running)
    {
        if (shared_stuff->written_by_you)
        {
            printf("You wrote: %s", shared_stuff->some_text);
            sleep(rand() % 4);
            shared_stuff->written_by_you = 0;
            if (strncmp(shared_stuff->some_text, "end", 3) == 0)
            {
                running = 0;
            }
        }
    }

    if (shmdt(shared_memory) == -1)
    {
        fprintf(stderr, "shmdt failed!\n");
        exit(-1);
    }

    if (shmctl(shmid, IPC_RMID, 0) == -1)
    {
        fprintf(stderr, "shmctl(IPC_RMID) failed!\n");
        exit(-1);
    }

    exit(0);
}

shm_write.c

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

#define SHARE_BUF 16 * 1024
#define TEXT_SZ 2048

struct shared_use_st
{
    int written_by_you;
    char some_text[TEXT_SZ];
};

int main()
{
    int running = 1;
    void *shared_memory = (void *)0;
    struct shared_use_st *shared_stuff;
    char buffer[TEXT_SZ];
    int shmid;

    // 获取
    shmid = shmget((key_t)1, SHARE_BUF, 0666 | IPC_CREAT);
    if (shmid == -1)
    {
        fprintf(stderr, "Shmget failed!\n");
        exit(-1);
    }

    //连接
    shared_memory = shmat(shmid, (void *)0, 0);
    if (shared_memory == (void *)-1)
    {
        fprintf(stderr, "Shmat failed!\n");
        exit(-1);
    }

    shared_stuff = (struct shared_use_st *)shared_memory;
    while (running)
    {
        while (shared_stuff->written_by_you == 1)
        {
            sleep(1);
            printf("Waiting for client...\n");
        }
        printf("Enter some text: ");
        fgets(buffer, TEXT_SZ, stdin);

        strncpy(shared_stuff->some_text, buffer, TEXT_SZ);
        shared_stuff->written_by_you = 1;

        if (strncmp(buffer, "end", 3) == 0)
        {
            running = 0;
        }
    }

    if (shmdt(shared_memory) == -1)
    {
        fprintf(stderr, "Shmdt failed\n");
        exit(-1);
    }

    exit(0);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我叫小八

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值