进程间通信(IPC)

共享内存

特性

  1. 性能最快
  2. 物理内存中开辟共享内存,进程在页表中将共享内存与地址空间形成映射
  3. 共享内存没有访问控制,不提供同步或互斥机制
  4. 共享内存生命周期不跟随进程,进程结束后共享内存不会自动释放,需要在进程中使用接口函数将共享内存释放,或使用命令行释放

编程实现步骤

  1. A进程创建共享内存并将共享内存映射到进程地址空间
  2. B进程获取共享内存并将共享内存映射到进程地址空间
  3. A、B进程通过共享内存交互数据
  4. A、B进程删除映射
  5. A进程释放共享内存

数据结构

shmid_ds

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 描述共享内存

struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) *///共享内存空间大小
__kernel_time_t shm_atime; /* last attach time *///挂接时间
__kernel_time_t shm_dtime; /* last detach time *///取消挂接时间
__kernel_time_t shm_ctime; /* last change time *///改变时间
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches *///进程挂接数
unsigned short shm_unused; /* compatibility */
void* shm_unused2; /* ditto - used by DIPC */
void* shm_unused3; /* unused */
};

ipc_perm

1
2
3
4
5
6
7
8
9
10
11
// IPC关键信息

struct ipc_perm{
__kernel_key_t key; //共享内存的唯一标识符
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode; //权限
unsigned short seq;
};

函数

ftok

1
2
3
4
5
6
7
8
9
10
// 获取唯一key

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

key_t ftok(const char *pathname, // 真实文件路径
int proj_id // 非零值
);

// 返回key值

shmget

1
2
3
4
5
6
7
8
9
10
11
12
// 创建共享内存

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

int shmget(key_t key, // key值,可以用ftok获取,也可以自己任意指定一个
size_t size, // 共享内存大小,一般申请4096byte的整数倍,即使不申请整数倍,系统也会凑整分配
int shmflg // 共享内存属性,一般设置为:IPC_CREAT | IPC_EXCL | 0666
);

// 成功返回共享内存ID,失败返回-1
// key值和共享内存ID都唯一对应一段共享内存,但key值是内核级的,供内核标识;共享内存ID是用户级的,供用户使用

shmctl

1
2
3
4
5
6
7
8
9
10
11
// 控制共享内存,一般用于释放共享内存

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

int shmctl(int shmid, // 共享内存ID
int cmd, // 控制指令,释放共享内存指令为IPC_RMID
struct shmid_ds *buf // 释放时使用空指针即可,nullptr
);

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

shmat

1
2
3
4
5
6
7
8
9
10
11
// 将共享内存映射到调用该函数的进程地址空间

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

void *shmat(int shmid, // 共享内存ID
const void *shmaddr, // 指定共享内存连接到进程地址空间的地址,设置为null则默认让系统指定
int shmflg // 权限,SHM_RDONLY(只读),SHM_REMAP(重映射到一个进程地址空间地址),0(系统默认)
);

// 返回映射到进程地址空间共享区起始地址

shmdt

1
2
3
4
5
6
// 删除共享内存与进程地址空间的映射关系,释放进程地址空间

int shmdt(const void *shmaddr // 共享内存映射到进程地址空间的地址
);

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

操作命令

查看共享内存

1
ipcs -m

释放共享内存

1
ipcrm -m shmid

示例程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// A.c
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>

#define SHM_PATH "/tmp" // 共享内存所在的文件路径

int main()
{
key_t key1 = ftok(SHM_PATH, 'A');
printf("key1 : %d\n", key1);
int shmid1 = shmget(key1, 4096, IPC_CREAT | IPC_EXCL | 0666);
printf("shmid1 : %d\n", shmid1);

key_t key2 = ftok(SHM_PATH, 'B');
printf("key2 : %d\n", key2);
int shmid2 = shmget(key2, 4096, IPC_CREAT | IPC_EXCL | 0666);
printf("shmid2 : %d\n", shmid2);
/*******************************************************************/

// 将共享内存附加到进程地址空间
int* a = (int *) shmat(shmid1, NULL, 0);
//int* b = (int *) shmat(shmid1, NULL, 0); // 多变量需要用数组指针方式存储
int* b = (int *) shmat(shmid2, NULL, 0);

// 在共享内存中创建变量
*a = 10;
*(a+1) = 20;
*b = 100;
*(b+1) = 200;

// 输出共享内存中变量的地址和值
printf("a的地址: %p\n", a);
printf("a的值: %d\n", *a);
printf("a+1的地址: %p\n", a+1);
printf("a+1的值: %d\n", *(a+1));
printf("b的地址: %p\n", b);
printf("b的值: %d\n", *b);
printf("b+1的地址: %p\n", b+1);
printf("b+1的值: %d\n", *(b+1));

// 等待程序B读取变量a的值
getchar();

// 输出共享内存中变量的地址和值
printf("a的地址: %p\n", a);
printf("a的值: %d\n", *a);
printf("a+1的地址: %p\n", a+1);
printf("a+1的值: %d\n", *(a+1));
printf("b的地址: %p\n", b);
printf("b的值: %d\n", *b);
printf("b+1的地址: %p\n", b+1);
printf("b+1的值: %d\n", *(b+1));

// 将共享内存从进程地址空间中分离
shmdt(a);
shmdt(b);

// 删除共享内存
shmctl(shmid1, IPC_RMID, NULL);
shmctl(shmid2, IPC_RMID, NULL);

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// B.c

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

#define SHM_PATH "/tmp" // 共享内存所在的文件路径

int main()
{
key_t key1 = ftok(SHM_PATH, 'A');
printf("key1 : %d\n", key1);
int shmid1 = shmget(key1, 4096, 0);
printf("shmid1 : %d\n", shmid1);

key_t key2 = ftok(SHM_PATH, 'B');
printf("key2 : %d\n", key2);
int shmid2 = shmget(key2, 4096, 0);
printf("shmid2 : %d\n", shmid2);
/*******************************************************************/

// 将共享内存附加到进程地址空间
int* a = (int *) shmat(shmid1, NULL, 0);
int* b = (int *) shmat(shmid2, NULL, 0);

// 输出共享内存中变量的地址和值
printf("a的地址: %p\n", a);
printf("a的值: %d\n", *a);
printf("a+1的地址: %p\n", a+1);
printf("a+1的值: %d\n", *(a+1));
printf("b的地址: %p\n", b);
printf("b的值: %d\n", *b);
printf("b+1的地址: %p\n", b+1);
printf("b+1的值: %d\n", *(b+1));

// 在共享内存中创建变量a
*a = 1;
*(a+1) = 2;
*b = 11;
*(b+1) = 22;

// 输出共享内存中变量的地址和值
printf("a的地址: %p\n", a);
printf("a的值: %d\n", *a);
printf("a+1的地址: %p\n", a+1);
printf("a+1的值: %d\n", *(a+1));
printf("b的地址: %p\n", b);
printf("b的值: %d\n", *b);
printf("b+1的地址: %p\n", b+1);
printf("b+1的值: %d\n", *(b+1));
// 将共享内存从进程地址空间中分离
shmdt(a);
//shmdt(b);

return 0;
}

匿名管道

特性

  1. 用于连接一个读进程和一个写进程以实现两者之间通信的一个“共享文件”
  2. 仅适用于具有亲缘关系的进程间通信,匿名管道无法被其他进程找到
  3. 半双工,需要全双工通信时可以创建两个管道
  4. 管道的生命周期跟随进程,进程结束后管道会释放
  5. 每个进程都持有一对管道的读端、写端,不同进程间的读端、写端不会互相影响
  6. 读写规则
    1. 管道所有写端都关闭情况下,当有进程从管道读端读取时,管道剩余数据读取完后,继续读取会返回0
    2. 管道写端没关闭,但也没进程向管道写端写入的情况下,当有进程从管道读端读取时,管道剩余数据读取完后,读端进程会阻塞
    3. 管道所有读端都关闭情况下,当有进程向管道写端写入时,进程会收到信号SIGPIPE,导致进程异常终止
    4. 管道读端没关闭,但也没进程从管道读端读取的情况下,当有进程向管道写端写入时,管道剩余容量写满后,写端进程会阻塞

编程实现步骤

  1. 父进程创建并打开匿名管道
  2. 父进程创建子进程
  3. 管道读写
    1. 父进程对管道的读端、写端进行操作
    2. 子进程对管道的读端、写端进行操作

函数

pipe

1
2
3
4
5
6
7
8
// 创建并打开匿名管道

#include <unistd.h>

int pipe(int fd[2] // 文件描述符,fd[0]表示读端,fd[1]表示写端
);

// 成功返回0, 失败返回错误代码

fork

1
2
3
4
5
6
7
8
9
10
// 父进程创建子进程

#include <sys/types.h>
#include <unistd.h>

pid_t fork();

// 父进程返回新创建子进程的进程ID
// 子进程返回0
// 失败返回负值

命名管道

特性

  1. 命名管道可以实现进程间通过同一个路径名看到同一份资源,这份资源以FIFO文件形式存在于文件系统中
  2. 不具备亲缘关系的进程间也可以使用
  3. 需要先创建,再打开(匿名管道创建即打开)

编程实现步骤

  1. 进程A选定路径创建命名管道
  2. 进程A根据选定的路径打开命名管道
  3. 进程B根据选定的路径打开命名管道
  4. 管道读写
    1. 进程A对管道读端、写端操作
    2. 进程B对管道读端、写端操作

函数

mkfifo

1
2
3
4
5
6
7
8
9
10
// 创建命名管道

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char* pathname, // 文件路径
mode_t mode // 文件权限
);

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

open

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 打开命名管道

#include<fcntl.h>
#include<unistd.h>

int open(const char* pathname, // 文件路径
int flags // 打开模式,O_RDONLY(只读),O_WRONLY(只写),O_RDWR(可读可写)
);

int open(const char* pathname, // 文件路径
int flags, // 打开模式,O_CREAT(创建)
mode_t mode // 文件权限
);

// 成功返回文件描述符fd,失败返回-1

close

1
2
3
4
5
6
7
8
9
// 关闭命名通道

#include<fcntl.h>
#include<unistd.h>

int close(int fd // 文件描述符
);

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

read

1
2
3
4
5
6
7
8
9
10
// 从管道(文件)读取内容

#include <unistd.h>

ssize_t read(int fd, // 文件描述符
void *buf, // 指向接收数据缓冲区的指针
size_t count // 读取字节数
);

// 成功返回实际读取的字节数,失败返回-1

wirte

1
2
3
4
5
6
7
8
9
10
// 向管道(文件)写入内容

#include <unistd.h>

ssize_t write(int fd, // 文件描述符
const void * buf, // 指向写入数据缓冲区的指针
size_t count // 写入字节数
);

// 成功返回实际写入的字节数,失败返回-1

操作命令

创建命名管道

1
2
3
mkfifo pathname

mkfifo /tmp/test

查看命名管道

1
2
3
ls -lF pathname

ls -lF /tmp/test

向命名管道写入

1
2
3
4
echo "xxx" > pathname
ls / > pathname

echo "Helloworld" > /tmp/test

从命名管道读取

1
2
3
cat < pathname

cat < /tmp/test

删除命名管道

1
2
3
rm pathname

rm /tmp/test

消息队列

特性

  1. 系统内核中的一个链表
  2. 发送进程将消息添加到消息队列末尾,接收进程从消息队列头部读取消息
  3. 多个进程可以同时向一个消息队列发送,也可以同时从一个消息队列中接收
  4. 通过结构体对信息打包
    1. 可以在结构体中自定义消息的规格细节,其中消息类型和消息内容是必须定义的
    2. 保证以消息为单位接收
    3. 能根据消息结构体中定义的消息类型接收消息

编程实现步骤

  1. A进程创建消息队列
  2. B进程获取消息队列
  3. 数据交互
  4. 任意进程关闭消息队列

数据结构

msgbuf

1
2
3
4
5
6
#include <sys/msg.h>

struct msgbuf {
long mtype; /* 消息的类型,必须为正数 */
char mtext[1]; /* 消息正文 */
};
  1. msg
1
2
3
4
5
6
7
8
#include <sys/msg.h>

struct msg {
struct msg *msg_next; /* 消息队列的下一条消息 */
long msg_type; /*消息类型*/
char *msg_spot; /* 消息正文的地址 */
short msg_ts; /* 消息正文的大小 */
};

msqid_ds

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/msg.h>

struct msqid_ds {
struct ipc_perm msg_perm; /* 消息的权限 */
struct msg *msg_first; /* 队列上第一条消息,即链表头 */
struct msg *msg_last; /* 队列中的最后一条消息,即链表尾 */
time_t msg_stime; /* 发送给队列的最后一条消息的时间 */
time_t msg_rtime; /* 从消息队列接收到的最后一条消息的时间 */
time_t msg_ctime; /* 最后修改队列的时间*/
ushort msg_cbytes; /* 队列上所有消息总的字节数 */
ushort msg_qnum; /* 在当前队列上消息的个数 */
ushort msg_qbytes; /* 队列最大的字节数 */
ushort msg_lspid; /* 发送最后一条消息的进程的pid */
ushort msg_lrpid; /* 接收最后一条消息的进程的pid */
};

函数

msgget

1
2
3
4
5
6
7
8
9
10
11
// 创建消息队列

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, // key值
int msgflg // 消息队列属性
);

// 成功返回消息队列ID,失败返回-1

msgctl

1
2
3
4
5
6
7
8
9
10
11
12
// 控制(删除)消息队列

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl(int msqid, // 消息队列ID
int cmd, // 控制命令,IPC_RMID(删除),
struct msqid_ds *buf // 控制命令为删除时,设为NULL
);

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

msgsend

1
2
3
4
5
6
7
8
9
10
11
12
13
// 向消息队列发送消息

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd(int msqid, // 消息队列ID
const void *msgp, // 指向待发送消息缓冲区的指针
size_t msgsz, // 除去消息类型(mtype)的消息字节数
int msgflg // 0:阻塞;IPC_NOWAIT:不阻塞
);

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

msgrcv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 从消息队列接收消息

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

ssize_t msgrcv(int msqid, // 消息队列ID
void *msgp, // 指存放消息缓冲区的指针
size_t msgsz, // 除去消息类型(mtype)的消息字节数
long msgtyp, // 消息类型
// 0:获取队列中第一条消息
// >0:获取类型为 msgtyp 的第一条消息
// <0:按优先级队列最小获取类型≤|msgtyp|的第一条消息
int msgflg // 0:阻塞;IPC_NOWAIT:不阻塞;
// MSG_EXCEPT:仅用于msgtyp>0的情况,表示获取类型不为msgtyp的消息
// MSG_NOERROR:如果消息数据正文内容大于msgsz,就将消息数据截断为msgsz
);

// 成功返回接受的消息字节数,失败返回-1

操作命令

查看消息队列

1
ipcs -q

查看消息队列对应的用户、发送和接收进程pid

1
ipcs -pq

查看该msgid的消息队列的详细情况

1
ipcs -q -i msgid

删除消息队列

1
ipcrm -Q key

示例程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// msgsend.c

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

struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[256]; /* message data */
};

int main(){
key_t key = ftok(".",'o');
printf("key = %x\n",key);

int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
if(msgid == -1){
printf("create queue failed!\n");
exit(1);
}

struct msgbuf buf1 = {1,"buf1"};
struct msgbuf buf2 = {2,"buf2"};
struct msgbuf buf3 = {3,"buf3"};
struct msgbuf buf4 = {1,"buf4"};

msgsnd(msgid,&buf1,sizeof(buf1.mtext),0);
msgsnd(msgid,&buf2,sizeof(buf2.mtext),0);
msgsnd(msgid,&buf3,sizeof(buf3.mtext),0);
msgsnd(msgid,&buf4,sizeof(buf4.mtext),0);

//msgctl(msgid,IPC_RMID,NULL);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// msgrcv.c

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

struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[256]; /* message data */
};

int main(){
key_t key = ftok(".",'o');
printf("key = %x\n",key);

int msgid = msgget(key, 0);
if(msgid == -1){
printf("create queue failed!\n");
exit(1);
}

struct msgbuf getbuf;

msgrcv(msgid,&getbuf,sizeof(getbuf.mtext),2,0);
printf("Received send message:%s\n",getbuf.mtext);

memset(getbuf.mtext, '\0', sizeof(getbuf.mtext));
msgrcv(msgid,&getbuf,sizeof(getbuf.mtext),3,0);
printf("Received send message:%s\n",getbuf.mtext);

memset(getbuf.mtext, '\0', sizeof(getbuf.mtext));
msgrcv(msgid,&getbuf,sizeof(getbuf.mtext),1,0);
printf("Received send message:%s\n",getbuf.mtext);

memset(getbuf.mtext, '\0', sizeof(getbuf.mtext));
msgrcv(msgid,&getbuf,sizeof(getbuf.mtext),1,0);
printf("Received send message:%s\n",getbuf.mtext);

msgctl(msgid,IPC_RMID,NULL);
return 0;
}