进程间通信
因为不同的进程用户地址空间都是相互隔离的,但是内核空间是每一个进程共享的,所以进程间的通信必然通过内核(空间)。
linux下查看IPC使用情况:
ipcs
一、管道
1)匿名管道
匿名管道存在于内存之中,不能在文件系统中找到匿名管道
相关指令 |
比如: ps -ef|grep docker
相关函数
int pipe2(int pipefd[2], int flags);
创建一个匿名管道,返回两个描述符,一个用于读(fd[0]),一个用于写(fd[1])。创建的管道其实就是内核中的一段缓存。
匿名管道一般用于父子进程之间的通信,因为一个管道有读写端,所以一般在父子进程中分别关掉一端,一个管道只用来单方向通信,如果需要互相读写,则使用两个管道。
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char **argv)
{
int pipefd[2];
char buf;
int pid;
if (pipe(pipefd) == -1){
perror("pipe");
exit(EXIT_FAILURE);
}
pid = fork();
if (pid == -1){
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0){
close(pipefd[1]);
while(read(pipefd[0], &buf, 1)>0){
write(STDOUT_FILENO, &buf, 1);
}
write(STDOUT_FILENO, "\n", 1);
close(pipefd[0]);
printf("Child exit\n");
exit(EXIT_SUCCESS);
}else{
close(pipefd[0]);
write(pipefd[1], "HAHAHAH", 7);
close(pipefd[1]);
wait(NULL);
printf("Parent exit\n");
exit(EXIT_SUCCESS);
}
}
2)有名管道
相关指令
创建管道:
mkfifo test
写管道
echo "tests" > test
读管道
echo < test
相关函数
int mkfifo(const char * pathname,mode_t mode);
使用mkfifo创建管道文件,之后把它作为文件来进行读写就可以了。
匿名管道和有名管道的数据量是有一定限制的,linux下缓冲区大小为1页也就是4k,所以管道适合数据量很小的通信。而且当读写速度不一致时,读端会阻塞,写端当缓存满了之后也会阻塞,影响通信的效率。
二、消息队列
消息队列是保存在内核中的消息链表 ,进程间规定好消息体(自定义的结构体)。发送端发送消息之后,数据存在于消息队列链表中,读取消息队列按照发送的顺序读取,当读取完所有的消息之后,内核把这个消息体删除掉。
消息队列有大小限制,所有的消息体的总大小也有限制。所以不适用于大量数据的传递。而且消息队列存在用户态到内核态的数据拷贝开销。
相较于管道,消息队列是以消息为最小单位传输的,而管道是连续字节流数据。
相关函数
1、创建和获取一个消息队列
int msgget(key_t, key, int msgflg);
key用来标识一个消息队列
msgflg是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg可以与IPC_CREAT做或操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被忽略,而只返回一个标识符。
返回值返回一个消息队列标识,失败返回-1
2、发送消息到消息队列
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
msgid使用msgget的返回值
msg_ptr是一个指向准备发送消息的指针,消息的结构体定义要求:
struct message{
long int message_type;
...
}
msgflg用于控制当前消息队列满或队列消息到达系统范围的限制时的操作
3、从消息队列获取消息
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);
4、消息队列操作
int msgctl(int msgid, int command, struct msgid_ds *buf);
command取值:
IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。
IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值
IPC_RMID:删除消息队列
buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。
示例
1、发送端
#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 msg[BUFSIZ];
};
int main()
{
int msgid;
struct msg_st snd_data;
msgid = msgget((key_t)123, 0666|IPC_CREAT);
if (msgid==-1){
perror("msgget");
exit(EXIT_FAILURE);
}
snd_data.msg_type = 11;
strcpy(snd_data.msg, "testdata");
if (msgsnd(msgid, (void*)&snd_data, BUFSIZ,0) == -1){
perror("msgsnd");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
2、接受端
#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 msg[BUFSIZ];
};
int main(){
struct msg_st rcv_data;
int msgid;
int msg_type=0;
msgid = msgget((key_t)123, 0666|IPC_CREAT);
if (msgid==-1){
perror("msgget");
exit(EXIT_FAILURE);
}
if (msgrcv(msgid, (void*)&rcv_data, BUFSIZ,msg_type, 0) == -1){
perror("msgrcv");
exit(EXIT_FAILURE);
}else{
printf("%s\n",rcv_data.msg);
}
if (msgctl(msgid, IPC_RMID, 0) == -1){
perror("remove msg");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
三、共享内存
共享内存就是把两个进程的物理地址映射到同一块物理地址上,这样两个进程在同一块物理内存上操作,避免了用户态到内核态的拷贝开销。
但是共享内存也有一个问题就是,进程之间的读取需要手动完成同步的操作,一般使用信号量来进行同步访问控制。
相关函数
1、分配共享内存
int shmget(key_t key, size_t size,int shmflg);
创建或打开一块共享内存区
key:进程间通信键值,ftok() 的返回值。
size:该共享存储段的长度(字节)。
shmflg:标识函数的行为及共享内存的权限,其取值如下:
IPC_CREAT:如果不存在就创建
IPC_EXCL: 如果已经存在则返回失败
位或权限位:共享内存位或权限位后可以设置共享内存的访问权限,格式和 open() 函数的 mode_t 一样(open() 的使用请点此链接),但可执行权限未使用。
返回值:
成功:共享内存标识符。
失败:-1。
2、映射共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg);
把一段已经分配好的共享内存映射到进程的数据段中。
shmid:共享内存标识符,shmget() 的返回值。
shmaddr:共享内存映射地址(若为 NULL 则由系统自动指定),推荐使用 NULL。
shmflg:共享内存段的访问权限和映射条件( 通常为 0 ),具体取值如下:
0:共享内存具有可读可写权限。
SHM_RDONLY:只读。
SHM_RND:(shmaddr 非空时才有效)
返回值:
成功:共享内存段映射地址( 相当于这个指针就指向此共享内存 )
失败:-1
3、解除共享内存的映射
int shmdt(const void *shmaddr);
把映射到共享内存的地址解除映射。
shmaddr:共享内存映射地址。
返回值:
成功:0
失败:-1
4、控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:共享内存标识符。
cmd:函数功能的控制,其取值如下:
IPC_RMID:删除。(常用 )
IPC_SET:设置 shmid_ds 参数,相当于把共享内存原来的属性值替换为 buf 里的属性值。
IPC_STAT:保存 shmid_ds 参数,把共享内存原来的属性值备份到 buf 里。
SHM_LOCK:锁定共享内存段( 超级用户 )。
SHM_UNLOCK:解锁共享内存段。
buf:shmid_ds 数据类型的地址(具体类型请点此链接 ),用来存放或修改共享内存的属性。
返回值:
成功:0
失败:-1
示例
1、写内存端
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/shm.h>
#include <sys/types.h>
struct shared_st
{
int writeable;
char data[BUFSIZ];
};
int main(){
int shmid;
void *shm=NULL;
struct shared_st *data=NULL;
shmid = shmget((key_t)123, sizeof(struct shared_st), 0666|IPC_CREAT);
if (shmid == -1){
perror("shmget");
exit(EXIT_FAILURE);
}
shm=shmat(shmid, 0,0);
if (shm == (void*)-1){
perror("shmat");
exit(EXIT_FAILURE);
}
printf("shared address %X\n", (int)shm);
data = (struct shared_st*)shm;
while(data->writeable!=1){
sleep(1);
}
strcpy(data->data, "A message from writer\n");
data->writeable = 0;
if(shmdt(shm) == -1)
{
perror("shmdt");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
2、读内存端
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/shm.h>
#include <sys/types.h>
struct shared_st
{
int writeable;
char data[BUFSIZ];
};
int main(){
int shmid;
void *shm=NULL;
struct shared_st *data=NULL;
shmid = shmget((key_t)123, sizeof(struct shared_st), 0666|IPC_CREAT);
if (shmid == -1){
perror("shmget");
exit(EXIT_FAILURE);
}
shm=shmat(shmid, 0,0);
if (shm == (void*)-1){
perror("shmat");
exit(EXIT_FAILURE);
}
printf("shared address %X\n", (int)shm);
data = (struct shared_st*)shm;
data->writeable = 1;
while(data->writeable!=0){
sleep(1);
}
if (strlen(data->data)>0){
printf("read data %s\n",data->data);
}
if (shmdt(shm)==-1){
perror("shmdt");
exit(EXIT_FAILURE);
}
if (shmctl(shmid, IPC_RMID, 0)==-1){
perror("remove shm");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
四、信号量
在使用共享内存的时候遇到数据同步的问题,可以使用信号量来解决。信号量主要用于进程间的互斥和同步,而不能用来传输数据。
信号量的两种操作
P:如果sv的值大于0则减一,如果sv值为0,则进程挂起
V:如果有其他的进程因为等待sv而挂起,则使它恢复运行,如果没有,则sv值加一
sv初始值为1
相关函数(System V)
(还有sem_init的一套函数(POSIX信号量),一般用于线程同步)
1、创建或者获取一个信号量
int semget(key_t key, int num_sems, int sem_flags);
key:信号量键值,可以理解为信号量的唯一性标记,可通过ftok获取。
num_sems:信号量的数目,一般为1,如果只是访问不创建可以为0.
sem_flags:有两个值,IPC_CREATE和IPC_EXCL,
IPC_CREATE表示若信号量已存在,返回该信号量标识符。
IPC_EXCL表示若信号量已存在,返回错误。
返回值:相应的信号量标识符,失败返回-1
2、改变信号量的值
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
**sem_id:**信号量标识符
sem_opa:结构如下
struct sembuf{
short sem_num;//除非使用一组信号量,否则它为0
short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
//一个是+1,即V(发送信号)操作。
short sem_flg;//SEM_UNDO:当操作的进程退出后,该进程对sem进行的操作将被取消;
//IPC_NOWAIT:在对信号量的操作不能执行的情况下,该操作立即返回;
//0:在对信号量的操作不能执行的情况下,该操作阻塞到可以执行为止
};
3、信号量初始化和删除
int semctl(int sem_id,int sem_num,int command,[union semun sem_union]);
command:有两个值SETVAL,IPC_RMID,分别表示初始化和删除信号量。
sem_union:可选参数,结构如下:
union semun{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
五、信号
查看linux下所有的信号:
kill -l
当进程接收到信号的时候,处理方式如下:
1)执行默认操作。对大多数的信号系统的默认处理方式就是终止进程。
2)捕获信号。当定义了信号处理函数的时候,会捕获信号并执行对应的处理函数。
比如:
void handler(int sig)
{
printf("%d\n",sig);
}
int main()
{
signal(SIGINT,fun);
...
}
3)忽略信号。
比如:
signal(SIGINT, SIG_IGN);
其中,SIGKILL和
SIGSTOP是不可以被捕获或者忽略的。
六、Socket
当互相通信的进程有可能不部署在同一台机器的时候,最好使用socket来通信。