十、进程间通信

1. 什么是进程间通信

Unix/Linux系统中每个进程都拥有独立的4G字节大小的虚拟内存空间。其中高地址的1G字节被映射到相同的物理内存区域,用于保存内核代码和数据。低地址的3G字节作为保存用户代码和数据的用户空间,被映射到彼此不同物理内存。因此同一个虚拟内存地址,在不同的进程中,会被映射到不同的物理内存区域,在多个进程之间以交换虚拟内存地址的方式交换数据是不可能的。鉴于进程之间天然的内存壁垒,为了能够在不同进程之间高效地交换数据,需要有一种专门的机制,这就是所谓的进程间通信(Inter-Process Communication, IPC)。

2. 简单的进程间通信

1) 命令行参数

用于进程的初始化设置

进程1组织命令行参数,然后通过execl创建进程2处理命令行参数
如:
进程1:有username,password两变量 ,通过execl作为命令行参数传入给进程2
execl(…, “login”, username, password, NULL)
进程2:得到 argv[1] = username, argv[2] = password

2)环境变量

用于进程的初始化设置

进程1组织环境变量,然后通过execle创建进程2处理环境变量
如:
进程1:有username,password 两变量 ,通过execle作为环境变量参数传入给进程2
sprintf(envp[0], “USERNAME=%s”, username);
sprintf(envp[1], “PASSWORD=%s”, password);
execle(…, envp);
进程2:envp[0]->USERNAME=minwei
    envp[1]->PASSWORD=tarena

3) wait/waitpid

获得终止信息

进程1通过fork/vfork+exec创建进程2,在进程2返回时或退出时(return …;exit/_exit/_Exit(…)) 。
可通过wait/waitpid(…, &status);中的status拿到退出状态码信息。

4) 内存映射文件

通过内存的方式操作共享文件,读写磁盘数据,速度慢但持久性好

通过将两进程的虚拟内存映射到同一块文件区域(区别下面的物理内存),这时可实现数据共享而进行进程间的通信。

5) 信号

简单,异步,信息量有限,效率不高,可靠性不佳

进程1可通过信号(现代的信号处理函数)及其附加值发送给进程2

3. 传统的进程间通信

1) 有名管道

非近亲进程之间的中等规模数据通信

进程1和进程2通过管道文件进行通信。
管道文件:有i节点没有数据块,是内存文件。
特点:全双工通信(双方可同时进行读写)

可通过命令行mkfifo <有名管道文件名>来创建有名管道文件

mkfifo函数

创建有名管道文件

 #include <sys/stat.h> 
 int mkfifo(const char*  pathname, model_t  mode); 

成功返回0,失败返回-1。
pathname - 文件路径
mode - 权限模式‘0666’
打开、关闭、读取和写入有名管道文件的方法与读写普通文件无异:open/read/write/close。

//写端
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#define FIFO_FILE "myfifo"
int main(void) 
{
    printf("创建管道...\n");
    if (mkfifo(FIFO_FILE, 0666) == -1) {
        perror("mkfifo");
        return -1;
    }
    printf("打开管道...\n");
    int fd = open(FIFO_FILE, O_WRONLY);//这里只写模式打开,所以在没有读取的情况下,会发生阻塞。    			
    if (fd == -1) {					   //这样做的目的是防止管道爆裂(因为管道文件没有数据块只有i节点)
        perror("open");
        return -1;
    }
    printf("发送数据...\n");
    for (;;) {
        printf("> ");
        char buf[1024];
        fgets(buf, sizeof(buf) / sizeof(buf[0]),
            stdin);
        if (!strcmp(buf, "!\n"))
            break;
        if (write(fd, buf, strlen(buf) * sizeof(
            buf[0])) == -1) {
            perror("write");
            return -1;
        }
    }
    printf("关闭管道...\n");
    if (close(fd) == -1) {
        perror("close");
        return -1;
    }
    printf("删除管道...\n");
    if (unlink(FIFO_FILE) == -1) {  //删除管道文件硬链接
        perror("unlink");
        return -1;
    }
    printf("完成!\n");
    return 0;
}
//读端
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#define FIFO_FILE "myfifo"
int main(void) 
{
    printf("打开管道...\n");
    int fd = open(FIFO_FILE, O_RDONLY);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    printf("接收数据...\n");
    for (;;) {
        char buf[1024] = {};
        ssize_t rb = read(fd, buf, sizeof(buf) -	//留一位作为空字符
            sizeof(buf[0]));
        if (rb == -1) {
            perror("read");
            return -1;
        }
        // 写端已关闭且管道无数据
        if (!rb)
            break;
        printf("< %s", buf);
    }
    printf("关闭管道...\n");
    if (close(fd) == -1) {
        perror("close");
        return -1;
    }
    printf("完成!\n");
    return 0;
}

编程模型如下:

进程A函数进程B
创建管道mkfifo
打开管道open打开管道
读写管道read/write读写管道
关闭管道close关闭管道
删除管道unlink

2) 无名管道

近亲进程之间的中等规模数据通信

在父子/兄弟进程中可采用两无名管道进行通信。
特点:半双工通信(双方不能同时进行读写。一方在写时,另一方便在读,反之一样)

pipe函数

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

成功返回0,失败返回-1。
pipefd - 输出两个文件描述符:
     pipefd[0]表示管道的读端
     pipefd[1]表示管道的写端

①父进程调用pipe函数在系统内核中创建无名管道对象,同时得到与该对象相关联的两个文件描述符,一个用于读取,另一个用于写入;
②父进程调用fork函数,创建子进程,子进程复制父进程的文件描述符表,因此子进程也同样拥有可用于读写管道对象的两个文件描述符。
③负责写数据进程关闭管道读端,即pipefd[0],而负责读数据的进程关闭管道的写端,即pipefe[1];
④父子进程通过各自持有的文件描述符,分别向管道写入和读取数据,待完成通信后再各自关闭所持有的文件描述符,内核中的无名管道对象即被释放。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) 
{
    printf("父进程:创建管道...\n");
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        perror("pipe");
        return -1;
    }
    printf("父进程:创建子进程...\n");
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }
    if (pid == 0) {
        printf("子进程:关闭写端...\n");
        close(pipefd[1]);
        printf("子进程:接收数据...\n");
        for(;;) {
            char buf[2014] = {};
            ssize_t rb = read(pipefd[0], buf,
                sizeof(buf) - sizeof(buf[0]));
            if (rb == -1) {
                perror("read");
                return -1;
            }
            if (!rb)
                break;
                
            printf("子进程收到数据:");
            fputs(buf, stdout);
        }
        printf("子进程:关闭读端...\n");
        close(pipefd[0]);
        printf("子进程:完成!\n");
        return 0;
    }
    printf("父进程:关闭读端...\n");
    close(pipefd[0]);
    printf("父进程:发送数据...\n");
    for (;;) {
    	usleep(100);//进行延时,使得子进程收到数据后父进程才打印提示信息
        char buf[1024];
        printf("父进程发送数据:");
        fgets(buf, sizeof(buf) / sizeof(buf[0]),
            stdin);
        if (!strcmp(buf, "!\n"))
            break;
        if (write(pipefd[1], buf, strlen(buf) *
            sizeof(buf[0])) == -1) {
            perror("write");
            return -1;
        }
    }
    printf("父进程:关闭写端...\n");
    close(pipefd[1]);
    if (wait(NULL) == -1) {
        perror("wait");
        return -1;
    }
    printf("父进程:完成!\n");
    return 0;
}

3) 基于管道通信的特殊情况

①从写端已被关闭的管道中读取
只要管道缓冲区中还有数据,依然可被正常读取,一直读到缓冲区空,这时read函数会返回0(既不是返回-1也不是阻塞),如同读到文件尾。

②向读端已被关闭的管道中写入
会直接触发SIGPIPE(13)信号。该信号的默认操作是终止执行写入动作的进程。但如果执行写入动作的进程已经事先将SIGPIPE(13)信号设置为忽略或捕获,这时虽然进程不会因为写入无读端的管道而被终止,但write函数会返回-1,并置errno为EPIPE

③在/usr/include/linux/limits.h头文件中定义的PIPE_BUF宏(4096)表示管道写缓冲区的大小。如果写管道时发现缓冲区中的空闲空间不足以容纳此次write调用所要写入的字节数,则write函数会阻塞,直到缓冲区中的空闲空间变得足够大为止。
如果同时有多个进程向同一个管道写入数据,而每次调用write函数写入的字节数都不大于BUF_SIZE,则这些write操作不会互相穿插(即原子化,atomic),反之单次写入的字节数超过了BUF_SIZE,则它们的write操作可能会相互穿插。
读取一个缓冲区为空的管道,只要其写端没有被关闭,读操作就会阻塞,除非该读文件描述符被设置为非阻塞(O_NONBLOCK),此时会立即返回失败,并置errno为EAGAIN。

4) 管道符号( | )的原理

命令行命令通过管道符( | )逐一向下流动输出,如下图:
在这里插入图片描述

示例:A | B

Shell进程: 
int pipefd[2]; 
pipe(pipefd); // 创建无名管道 
vfork(); //产生一个子进程1 
子进程1:
    close(pipefd[0]); // 关闭无名管道的读端
    dup2(pipefd[1], STDOUT_FILENO); // 写端=标准输出
    exec(A) // 创建A进程,继承了原进程的文件描述符表
            // 在A进程中,写端=标准输出
            // printf/puts将数据写入无名管道的写端 
vfork(); // 产生一个子进程2
    子进程2:
    close(pipefd[1]); // 关闭无名管道的写端
    dup2(pipefd[0], STDIN_FILENO); // 读端=标准输入
    exec(B) // 创建B进程,继承了原进程的文件描述符表
            // 在B进程集中,读端=标准输入
            // scanf/gets从无名管道的读端读取数据 ```
            A和B就成为协作进程,A写入数据进管道,B从管道中读取A写入的数据。
//output.c
#include <stdio.h>
int main(void) 
{
    int x = 123, y = 456;
    printf("%d %d\n", x, y);
    return 0;
}
//input.c
#include <stdio.h>
int main(void) 
{
    int x, y;
    scanf("%d%d", &x, &y);
    printf("%d+%d=%d\n", x, y, x + y);
    return 0;
}

此时可通过管道符号将output的输出给到input的输入:./output | ./input
其代码逻辑实现如下:

//shell.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
int main(void) 
{
    // 创建无名管道
    // 无名管道读端:pipefd[0]
    // 无名管道写端:pipefd[1]
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        perror("pipe");
        return 0;
    }
    // 创建子进程,执行输出进程
    pid_t pid = vfork();
    if (pid == -1) {
        perror("vfork");
        return -1;
    }
    if (pid == 0) {
        // 关闭无名管道读端
        close(pipefd[0]);
        // 复制无名管道写端到标准输出
        if (dup2(pipefd[1],
            STDOUT_FILENO) == -1) {
            perror("dup2");
            _exit(-1);
        }
        // 创建输出进程
        // 该进程所有向标准输出的写操作
        // 实际都是在写入无名管道的写端
        if (execl("./output", "output",
            NULL) == -1) {
            perror("execl");
            _exit(-1);
        }
    }
    // 创建子进程,执行输入进程
    if ((pid = vfork()) == -1) {
        perror("vfork");
        return -1;
    }
    if (pid == 0) {
        // 关闭无名管道写端
        close(pipefd[1]);
        // 复制无名管道读端到标准输入
        if (dup2(pipefd[0],
            STDIN_FILENO) == -1) {
            perror("dup2");
            _exit(-1);
        }
        // 创建输入进程
        // 该进程所有从标准输入的读操作
        // 实际都是在读取无名管道的读端
        if (execl("./input", "input",
            NULL) == -1) {
            perror("execl");
            _exit(-1);
        }
    }
    close(pipefd[0]);
    close(pipefd[1]);
    for (;;)
        if (wait(NULL) == -1) {
            if (errno != ECHILD) {
                perror("wait");
                return -1;
            }
            break;
        }
    return 0;
}

4. XSI进程间通信(SVR4)

1) IPC对象的标识符(ID)和键(KEY)

IPC对象在系统内核中的唯一名称用键(KEY)表示。不同的进程可以通过键来引用该IPC对象。一旦进程获得了该IPC对象,即通过其标识(ID)(可能一样可能不一样,取决于系统)来称谓该对象。
在这里插入图片描述

ftok函数(file to key)

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

成功返回IPC对象的键,失败返回-1。
pathname – 一个真实存在的路径,使用该路径的i节点号
proj_id – 项目ID,仅低8为有效,-128~127 / 0~255

相同项目使用相同的pathname和proj_id,保证key的一致性
不同项目使用不同的pathname或proj_id,避免key发生冲突

2) IPC对象的编程接口

Ⅰ.创建或获取IPC对象

// 共享内存 
int shmget(key_t key, size_t size, int shmflg); 
// 消息队列
int msgget(key_t key, int msgflg); 
// 信号量集 
int semget(key_t key, int nsems, int semflg);

Ⅱ.控制或销毁IPC对象

// 共享内存
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
// 消息队列
int msgctl(int msqid, int cmd, struct msqid_ds* buf);
// 信号量集
int semctl(int semid, int semnum, int cmd, union semun arg);

cmd - 控制命令,可取以下值:
        IPC_STAT: 获取IPC对象的属性
        IPC_SET: 设置IPC对象的属性
        IPC_RMID: 删除IPC对象

Ⅲ.IPC对象的权限结构

struct ipc_perm {
   key_t                   __key; 	// 键
   uid_t                   uid;     // 拥有者用户
   gid_t                   gid;     // 拥有者组
   uid_t                   cuid;    // 创建者用户
   gid_t                   cgid;    // 创建者组
   unsigned short mode; 			//  权限
   unsigned short __seq;  			// 序号
};

其中只有uid、gid和model三个字段在创建完成以后还可以被修改。

3) 共享内存

大数据量的快速数据通信,缺乏同步机制,需要依赖其它IPC(管道、信号等来决定什么时候读或写)机制实现同步

通过将两进程的虚拟内存映射到同一块物理内存(区别上面的文件区域),这时可实现数据共享而进行进程间的通信。

通过共享内存实现进程间通信,可以直接访问由系统内核维护的公共内存区域,不需要额外构建用户缓冲区,也不需要在用户缓冲区和内核缓冲区之间来回复制数据。因此共享内存是速度最快的进程间通信机制。但是共享内存因为缺乏必要的同步机制,往往需要借助其它进程间通信策略提供某种形式的停等机制。

直接由内核调度,无需申请内存空间作为缓冲区。
优点:时间效率高于消息队列。
缺点:可能会导致数据的缺失和重写问题。

两个或者更多进程,共享一块由系统内核负责维护的物理内存,其地址空间通常被映射到每个进程虚拟内存堆和栈之间的不同区域。

可通过命令行ipcs -m查看共享内存

共享内存的属性结构:

struct shmid_ds {
    struct ipc_perm 		shm_prem;  		// 权限结构
    size_t                  shm_segsz;  	// 字节数
    time_t                  shm_atime;   	// 最后加载时间
    time_t                  shm_dtime;   	// 最后卸载时间
    time_t                  shm_ctime;   	// 最后改变时间
    pid_t                   shm_cpid;   	// 创建进程的PID
    pit_t                   shm_lpid;  		// 最后加(卸)载进程的PID
    shmatt_t             	shm_nattch; 	// 当前加载计数
    ... 
}; 

shmget函数

创建或获取IPC对象

int shmget(key_t key, size_t size, int shmflg); 

size - 共享内存的字节数,按页向上取整。获取已有共享内存对象时可置0。
shmflg - 创建标志 ,可取以下值:
    0 - 获取,不存在即失败。
    IPC_CREATE - 创建兼获取,不存在即创建,已存在直接获取。
    IPC_EXCL - 不存在即创建,已存在直接报错。

shmat函数

加载共享内存到虚拟内存,建立虚拟内存和物理内存间的映射

void* shmat(int shmid, const void* shmaddr, int shmflags);

成功返回共享内存起始地址,失败返回void*类型的-1。
shmid - 共享内存标识
shmaddr - 共享内存起始地址,置NULL由系统内核选择
shmflags - 加载标志,可取以下值:
     0:可读可写
     SHM_RDONLY:只读
     SHM_RND:若shmaddr非空且不是页边界,则将其调整至页边界。(如:起始地址是10、20则调至0边界,4000、4050则调至4096边界)

shmdt函数

卸载共享内存

int shmdt(const void* shmaddr); 

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

shmat函数负责将给定共享内存映射到调用进程的虚拟内存空间,返回映射区的起始地址,同时将系统内核中共享内存对象的加载计数(shm_nattch)加1。调用进程在获得shmat函数返回的共享内存起始地址以后,就可以像访问普通内存一样访问该共享内存中的数据。

shmdt函数负责从调用进程的虚拟内存中解除shmaddr所指向的映射区到共享内存的映射,同时将系统内核中共享内存对象的加载计数(shm_nattch)减1。因此加载计数为0的共享内存必定是没有任何进程使用的。

shmctl(…,IPC_RMID, …)调用可以用于销毁共享内存,但并非真的销毁,而只是做一个销毁标记,禁止任何进程对该共享内存形成新的加载,但已有的加载依然保留。只有当其使用者们纷纷卸载,直至其加载计数降为0时,共享内存才会真的被销毁。

//wshm.c
#include <stdio.h>
#include <unistd.h>
#include <sys/shm.h> 
int main(void) 
{
    printf("创建共享内存...\n");
    key_t key = ftok(".", 100);
    if (key == -1) {
        perror("ftok");
        return -1;
    }
    int shmid = shmget(key, 4096, 0644 |
        IPC_CREAT | IPC_EXCL);
    if (shmid == -1) {
        perror("shmget");
        return -1;
    }
    printf("加载共享内存...\n");
    void* shmaddr = shmat(shmid, NULL, 0);
    if (shmaddr == (void*)-1) {
        perror("shmat");
        return -1;
    }
    printf("写入共享内存...\n");
    sprintf(shmaddr, "我是%d进程写入的数据。",
        getpid());
    printf("按<回车>卸载共享内存"  //按下回车后,共享内存连接数就会减一
        "(0x%08x/%d)...\n", key, shmid);//0x为普通字符,输出的时候会原样输出为0x。
        								//%08x为整型以16进制方式输出的格式字符串,
        								//会把后续对应参数的整型数字,以16进制输出。
        								//08的含义为,输出的16进制值占8位,不足部分左侧补0。
        								//如:printf("0x%08x", 0x1234);会输出0x00001234。
    getchar();
    if (shmdt(shmaddr) == -1) {
        perror("shmdt");
        return -1;
    }
    printf("按<回车>销毁共享内存"   				//按下回车后,且共享内存连接数为0,创建的共享内存才会真正的被销毁		   
        "(0x%08x/%d)...\n", key, shmid);		//(一般哪个进程创建共享内存,哪个进程就进行销毁)     
    getchar();									//如果不进行销毁,这个共享内存会一直存在
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        return -1;
    }
    printf("完成!\n");
    return 0; 
}

可通过命令行ipcrm -m <共享内存的id号>对共享内存进行销毁

//rshm.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/shm.h> 
void shmstat(int shmid) 
{
    struct shmid_ds shm;
    if (shmctl(shmid, IPC_STAT, &shm) == -1) {
        perror("shmctl");
        exit(-1);
    }
    printf("                键:0x%08x\n",
        shm.shm_perm.__key);
    printf("            用户ID:%u\n",
        shm.shm_perm.uid);
    printf("              组ID:%u\n",
        shm.shm_perm.gid);
    printf("       创建者用户ID:%u\n",
        shm.shm_perm.cuid);
    printf("         创建者组ID:%u\n",
        shm.shm_perm.cgid);
    printf("               权限:%#o\n",
        shm.shm_perm.mode);
    printf("             序列号:%u\n",
        shm.shm_perm.__seq);
    printf("         大小(字节):%u\n",
        shm.shm_segsz);
    printf("        最后加载时间:%s",
        ctime(&shm.shm_atime));
    printf("        最后卸载时间:%s",
        ctime(&shm.shm_dtime));
    printf("        最后改变时间:%s",
        ctime(&shm.shm_ctime));
    printf("         创建进程PID:%u\n",
        shm.shm_cpid);
    printf("最后加载/卸载进程PID:%u\n",
        shm.shm_lpid);
    printf("        当前加载计数:%ld\n",
        shm.shm_nattch); } 
int main(void) 
{
    printf("获取共享内存...\n");
    key_t key = ftok(".", 100);
    if (key == -1) {
        perror("ftok");
        return -1;
    }
    int shmid = shmget(key, 0, 0);
    if (shmid == -1) {
        perror("shmget");
        return -1;
    }
    printf("加载共享内存...\n");
    void* shmaddr = shmat(shmid, NULL, 0);
    if (shmaddr == (void*)-1) {
        perror("shmat");
        return -1;
    }
    printf("读取共享内存...\n");
    printf("%s\n", (char*)shmaddr);
    shmstat(shmid);
    printf("按<回车>卸载共享内存...\n"); //断开共享内存连接,连接数减一
    getchar();
    if (shmdt(shmaddr) == -1) {
        perror("shmdt");
        return -1;
    }
    shmstat(shmid);
    printf("完成!\n");
    return 0; 
}

4) 消息队列

天然的同步性,根据类型做细分,适用于中等规模数据通信

进程1-----消息3|消息2|消息1---->进程2(以队列的形式逐条发送消息)
消息队列需申请内存空间,经内核调度。
优点:消息有序传递,不易发送消息数据的缺失与重写。
缺点:时间效率低于共享内存。

消息队列是由单个的类型各异的一系列消息结构组成的链表
类型各异的消息结构组成外部的大队列,在其内部可根据其类型划分为子队列,但无论是大队列还是小队列都是先进先出。

最大可发送消息字节数:两页8192(8K)
最大全队列消息字节数:四页16384(16K)
最大全系统消息队列数:16
最大全系统消息总个数:262144

可通过命令行ipcs -q查看消息队列

msgget函数

#include <sys/msg.h> 
int msgget(key_t key, int msgflg);

msgflg - 创建标志 ,可取以下值:
    0 - 获取,不存在即失败。
    IPC_CREATE - 创建兼获取,不存在即创建,已存在直接获取。
    IPC_EXCL - 不存在即创建,已存在直接报错。

msgsnd函数

发送消息

int msgsnd(int msqid, const void* msgp,  size_t msgsz, int msgflg)

成功返回0,失败返回-1。
msqid – 消息队列标识
msgp – 指针 指向 消息类型(4字节) 后面跟着消息数据
msgsz – 消息数据的长度,不包括消息头部的消息类型
msgflg – 0: 阻塞
     IPC_NOWAIT:非阻塞
     …

msgsnd函数的msgp参数所指向的内存中包含4个字节大小的消息类型,其值必须大于0。
但该函数的msgsz参数所表示的期望发送字节数却不包含消息类型所占的4个字节。

如果系统内核中的消息未达上限,则msgsnd函数会将欲发送消息加入消息队列并立即返回0,否则该函数会阻塞,直到系统内核允许加入新消息为止(比如有消息因被接收而离开消息队列)。
若msgflg参数中包含IPC_NOWAIT位,则msgsnd函数在系统内核中的消息已达上限的情况下不会阻塞,而是返回-1,并置errno为EAGAIN。

msgrcv函数

接收消息

int msgrcv(int msqid, void* msgp,  size_t msgsz, long msgtyp, int msgflg);  

成功返回实际接收到的消息数据的长度,失败返回-1。
msqid – 消息队列标识
msgp – 指针 指向 消息类型(4字节) 后面跟着消息数据缓冲区(应略大于要接受的数据)
msgsz – 消息数据的长度,不包括消息头部的消息类型
msgtyp – 消息类型,msgtyp可取以下值:
      0 - 提取消息队列中的第一条消息而无论其类型。
    >0 - msgflg不含MSG_EXCEPT位,提取第一条类型为msgtyp的消息,msgflg含MSG_EXCEPT位,提取第一条类型不为msgtyp的       消息。
    <0 - 提取队列中类型小于或等于|msgtyp|绝对值的消息,类型越小的越先被提取。
msgflg –   0: 阻塞
     IPC_NOWAIT:非阻塞
    …

注意msgrcv函数的msgp参数所指向的内存块中包含4字节的消息类型,其值由该函数输出,但该函数的msgsz参数所表示的是期望接收字节数以及该函数所返回的实际接收字节数都不包含消息类型4个字节。

若存在与msgtyp参数匹配的消息,但是数据长度大于msgsz参数,且msgflg参数包MSG_NOERROR位,则只截取该消息数据的前msgsz字节返回,剩余部分直接丢弃;但如果msgflg参数不包MSG_NOERROR位,则不处理该消息,直接返回-1,并置errno为E2BIG。

msgrcv函数根据msgtyp参数对消息队列中消息有选择地接收,只有满足条件的消息才会被复制到应用程序缓冲区并从内核中删除。如果满足msgtyp条件的消息不只一条,则按照先进先出的规则提取。
若消息队列中有可接收消息,则msgrcv函数会将该消息移出消息队列,并立即返回所接收到的消息数据字节数,表示接收成功,否则此函数会阻塞,直到消息队列中有可接收消息为止。若msgflg参数包含IPC_NOWAIT位,则msgrcv函数在消息队列中没有可接收消息的情况下不会阻塞,而是返回-1,并置errno为ENOMSG。

wmsg.c
#include <stdio.h>
#include <string.h>
#include <sys/msg.h>
int main(void) 
{
    printf("创建消息队列...\n");
    key_t key = ftok(".", 100);
    if (key == -1) {
        perror("ftok");
        return -1;
    }
    int msqid = msgget(key, 0644 | IPC_CREAT |
        IPC_EXCL);
    if (msqid == -1) {
        perror("msgget");
        return -1;
    }
    printf("向消息队列(0x%08x/%d)发送数据...\n",
        key, msqid);
    for (;;) {
        printf("> ");
        struct {
            long mtype;
            char mtext[1024];
        }   msgbuf = {1234, ""};
        fgets (msgbuf.mtext, sizeof(
            msgbuf.mtext) / sizeof(
            msgbuf.mtext[0]), stdin);
        if (!strcmp(msgbuf.mtext, "!\n"))
            break;
        if (msgsnd(msqid, &msgbuf, strlen(
            msgbuf.mtext) * sizeof(
            msgbuf.mtext[0]), 0) == -1) {
            perror("msgsend");
            return -1;
        }
    }
    printf("销毁消息队列(0x%08x/%d)...\n",
        key, msqid);
    if (msgctl(msqid, IPC_RMID, NULL) == -1) {
        perror("msgctl");
        return -1;
    }
    printf("完成!\n");
    return 0;
}
rmsg.c
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/msg.h>
int main(void) 
{
    printf("获取消息队列...\n");
    key_t key = ftok(".", 100);
    if (key == -1) {
        perror("ftok");
        return -1;
    }
    int msqid = msgget(key, 0);
    if (msqid == -1) {
        perror("msgget");
        return -1;
    }
    printf("从消息队列(0x%08x/%d)接收消息...\n",
        key, msqid);
    for (;;) {
        struct {
            long mtype;
            char mtext[1024];
        }   msgbuf = {};
        ssize_t msgsz = msgrcv(msqid, &msgbuf,
            sizeof(msgbuf.mtext) - sizeof(
            msgbuf.mtext[0]), 1234,
            MSG_NOERROR | IPC_NOWAIT);
        if (msgsz == -1)
            if (errno == EIDRM) {   //EIDRM是因消息队列被销毁而出现的错误码
                printf("消息队列(0x%08x/%d)"
                    "已销毁!\n", key, msqid);
                break;
            }
            else if (errno == ENOMSG) {  
                printf("现在没有消息,"
                    "干点别的...\n");
                sleep(1);
            }
            else {
                perror("msgrcv");
                return -1;
            }
        else
            printf("%04d< %s", msgsz,
                msgbuf.mtext);
    }
    printf("完成!\n");
    return 0;
}

5) 信号量集

多数进程竞争少数资源

资源的需求者多于资源本身,如何协调有限资源在多数需求者之间的分配,以使每个资源需求者都有相对均衡的几率获得其所要求的资源。

系统内核中为每一种资源维护一个资源计数器,其值为当前空闲资源的数量,每当一个进程试图获取该种资源时,会先尝试减少其计数器的值,如果该计数器的值够减(计数器>=0),则说明空闲资源足够,该进程即获得资源,如果该计数器的值不够减,则说明空闲资源不够,该进程即进入等待模式,等候其它拥有该种资源的进程释放资源。任何一个拥有该种资源的进程,一旦决定释放该资源,都必须将其计数器的值予以增加,以表示空闲资源量的增加,为其它等候该资源的进程提供条件。

用一个信号量表示一种类型资源,其值为该类型资源的空闲数量。用由多个信号量组成的信号量集表示多种类型的资源。
在这里插入图片描述

semget函数

创建或获取信号量集

#include <sys/sem.h> 
int semget(key_t key, int nsems, int semflg);

成功返回信号量集标识符,失败返回-1。
key - 信号量集键
nsems - 信号量个数,即资源的种类数
semflg - 创建标志,可取以下值:
    0 - 获取,不存在即失败
    IPC_CREAT - 创建,不存在即创建,已存在即获取
    IPC_EXCL - 排斥,已存在即失败

semop函数

操作信号量集:减操作->拥有资源… …释放资源->加操作

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

成功返回0,失败返回-1。
semid - 信号量集标识
sops - 操作结构数组,sops指针指向一个struct sembuf类型的结构体数组(因此一个struct sembuf结构体操作一     个信号量),其中每个元素都是一个struct sembuf 类型的结构体,该结构体包含三个字段,用于表示针     对信号量集中的一个特定信号量的特定操作。
nsops - 操作结构数组长度

struct sembuf {
    unsigned short sem_num; 	// 信号量编号(集合索引)
    short           sem_op;     // 操作数( - 获取/ + 释放)
    short          sem_flg;     // 操作标志(0 / IPC_NOWAIT)                   
};

如果sem_op字段的值为负,则从semid信号量集第sem_num个信号量的值中减去|sem_op|(绝对值),以表示对资源的获取;如果不够减(信号量的值不能为负),则此函数会阻塞,直到够减为止,以表示对资源的等待,但如果sem_flg包含IPC_NOWAIT位,则即使不够减也不会阻塞,而是返回-1,并置errno为EAGAIN。

semctl函数

销毁或控制信号量集

int semctl(int semid, int semnum, int cmd, ...); 

成功返回0或其它与cmd有关的值,失败返回-1。

Ⅰ.销毁信号量集

int semctl(int semid, 0, IPC_RMID); 

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

Ⅱ.获取信号量集中每个信号量的值

unsigned short array[4]; // 每个信号量的值,信号量不能为负,所以用unsigned
int semctl(int semid, 0, GETALL, array); 

成功返回0,并获取每个信号量的值(个数)给到array数组中,失败返回-1。

Ⅲ.设置信号量集中每个信号量的值

unsigned short array[4] = {5, 5, 5, 5}; // 每个信号量的值
int semctl(int semid, 0, SETALL, array);

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

Ⅳ.获取信号量集中特定信号量的值

int semctl(int semid, int semnum, GETVAL);

成功返回semid信号量集中第semnum个信号量的值,失败返回 -1。

Ⅴ.设置信号量集中特定信号量的值

int semctl(int semid, int semnum, SETVAL, int val);

成功返回 0,并将semid信号量集中第semnum个信号量的值设置为val,失败返回 -1。

csem.c
#include <stdio.h>
#include <errno.h>
#include <sys/sem.h>
int pmenu(void) 
{
    printf("--------------------\n");
    printf("    迷你图书馆\n");
    printf("--------------------\n");
    printf("[1] 借《三国演义》\n");
    printf("[2] 还《三国演义》\n");
    printf("[3] 借《水浒传》\n");
    printf("[4] 还《水浒传》\n");
    printf("[5] 借《红楼梦》\n");
    printf("[6] 还《红楼梦》\n");
    printf("[7] 借《西游记》\n");
    printf("[8] 还《西游记》\n");
    printf("[0] 退出\n");
    printf("--------------------\n");
    printf("请选择:");
    int sel = -1;
    scanf("%d", &sel);
    return sel;
}
int pleft(int semid, unsigned short semnum) 
{
    int val = semctl(semid, semnum, GETVAL);
    if (val == -1) {
        perror("semctl");
        return -1;
    }
    printf("还剩%d册。\n", val);
    return 0;
}
int borrow(int semid, unsigned short semnum) 
{
    struct sembuf sops = {
        semnum, -1, /*0*/IPC_NOWAIT};
    if (semop(semid, &sops, 1) == -1) {
        if (errno != EAGAIN) {
            perror("semop");
            return -1;
        }
        printf("暂时无书,下回再试。\n");
        return 0;
    }
    printf("恭喜恭喜,借阅成功。\n");
    return pleft(semid, semnum);
}
int revert(int semid, unsigned short semnum) 
{
    struct sembuf sops = {semnum, 1, 0};
    if (semop(semid, &sops, 1) == -1) {
        perror("semop");
        return -1;
    }
    printf("好借好还,再借不难。\n");
    return pleft(semid, semnum);
}
int main(void) 
{
    printf("创建信号量集...\n");
    key_t key = ftok(".", 100);
    if (key == -1) {
        perror("ftok");
        return -1;
    }
    int semid = semget(key, 4, 0644 | IPC_CREAT |
        IPC_EXCL);
    if (semid == -1) {
        perror("semget");
        return -1;
    }
    printf("初始化信号量...\n");
    unsigned short semarr[] = {5, 5, 5, 5};
    if (semctl(semid, 0, SETALL, semarr) == -1) {
        perror("semctl");
        return -1;
    }
    int quit = 0;
    while (!quit) {
        int sel = pmenu();
        switch (sel) {
            case 0:
                quit = 1;
                break;
            case 1: // 借《三国演义》-> 0
            case 3: // 借《水浒传》  -> 1
            case 5: // 借《红楼梦》  -> 2
            case 7: // 借《西游记》  -> 3
                if (borrow(semid, sel / 2) == -1)
                    return -1;
                break;
            case 2: // 还《三国演义》-> 0
            case 4: // 还《水浒传》  -> 1
            case 6: // 还《红楼梦》  -> 2
            case 8: // 还《西游记》  -> 3
                if (revert(semid,
                    (sel - 1) / 2) == -1)
                    return -1;
                break;
            default:
                printf("无效选择!\n");
                scanf("%*[^\n]");//忽略所有非换行符
                scanf("%*c"); //忽略换行符
                break;
        }
    }
    printf("销毁信号量集...\n");
    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl");
        return -1;
    }
    printf("完成!\n");
    return 0;
}
gsem.c
#include <stdio.h>
#include <errno.h>
#include <sys/sem.h>
int pmenu(void) 
{
    printf("--------------------\n");
    printf("    迷你图书馆\n");
    printf("--------------------\n");
    printf("[1] 借《三国演义》\n");
    printf("[2] 还《三国演义》\n");
    printf("[3] 借《水浒传》\n");
    printf("[4] 还《水浒传》\n");
    printf("[5] 借《红楼梦》\n");
    printf("[6] 还《红楼梦》\n");
    printf("[7] 借《西游记》\n");
    printf("[8] 还《西游记》\n");
    printf("[0] 退出\n");
    printf("--------------------\n");
    printf("请选择:");
    int sel = -1;
    scanf("%d", &sel);
    return sel;
}
int pleft(int semid, unsigned short semnum) 
{
    int val = semctl(semid, semnum, GETVAL);
    if (val == -1) {
        perror("semctl");
        return -1;
    }
    printf("还剩%d册。\n", val);
    return 0;
}
int borrow(int semid, unsigned short semnum) 
{
    struct sembuf sops = {
        semnum, -1, /*0*/IPC_NOWAIT};
    if (semop(semid, &sops, 1) == -1) {
        if (errno != EAGAIN) {
            perror("semop");
            return -1;
        }
        printf("暂时无书,下回再试。\n");
        return 0;
    }
    printf("恭喜恭喜,借阅成功。\n");
    return pleft(semid, semnum);
}
int revert(int semid, unsigned short semnum) 
{
    struct sembuf sops = {semnum, 1, 0};
    if (semop(semid, &sops, 1) == -1) {
        perror("semop");
        return -1;
    }
    printf("好借好还,再借不难。\n");
    return pleft(semid, semnum);
}
int main(void) 
{
    printf("获取信号量集...\n");
    key_t key = ftok(".", 100);
    if (key == -1) {
        perror("ftok");
        return -1;
    }
    int semid = semget(key, 0, 0);
    if (semid == -1) {
        perror("semget");
        return -1;
    }
    int quit = 0;
    while (!quit) {
        int sel = pmenu();
        switch (sel) {
            case 0:
                quit = 1;
                break;
            case 1: // 借《三国演义》-> 0
            case 3: // 借《水浒传》  -> 1
            case 5: // 借《红楼梦》  -> 2
            case 7: // 借《西游记》  -> 3
                if (borrow(semid, sel / 2) == -1)
                    return -1;
                break;
            case 2: // 还《三国演义》-> 0
            case 4: // 还《水浒传》  -> 1
            case 6: // 还《红楼梦》  -> 2
            case 8: // 还《西游记》  -> 3
                if (revert(semid,
                    (sel - 1) / 2) == -1)
                    return -1;
                break;
            default:
                printf("无效选择!\n");
                scanf("%*[^\n]");//忽略所有非换行符
                scanf("%*c"); //忽略换行符
                break;
        }
    }
    printf("完成!\n");
    return 0;
}

6) IPC命令

查看IPC对象

ipcs -m (m=memory)共享内存对象
ipcs -q (q=queue)消息队列对象
ipcs -s (s=semphore)信号量集对象
ipcs -a (a=all) 全部IPC对象

删除IPC对象

ipcrm -m <共享内存对象标识>
ipcrm -q <消息队列对象标识>
ipcrm -s <信号量集对象标识>

5. 套接字进程间通信(BSD)

进程1和进程2通过本地套接字文件进行通信。
本地套接字文件:有i节点没有数据块,是内存文件。
好处:以一种统一的编程模式和接口库,处理网络和本机通信。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值