一、管道通信,共享内存,消息队列,信号通信是进程间通信方式。信号量是进程间同步的机制。
进程间通信的原因:数据传输,资源共享,通知事件,进程控制。
二、管道通信
(1)管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道尾部写入数据,另一个进程(读进程)从管道的头部读出数据。两个程序之间传递数据的一种简单方法是使用popen和pclose。
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
popen函数允许一个程序将另一个程序作为新进程来启动,并可以传递数据给它或者通过它接收数据。command字符串是要运行的程序名和相应的参数。type必须是"r"或"w"。
如果type是"r",被调程序的输出就可以被调用程序使用,调用程序利用popen函数返回的FILE *文件流指针,可以读取被调程序的输出;如果type是"w",调用程序就可以向被调程序发送数据,而被调程序可以在自己的标准输入上读取这些数据。
pclose函数只在popen启动的进程结束后才返回。如果调用pclose时它仍在运行,pclose将等待该进程的结束。
无名管道:
无名管道由pipe()函数创建:
int pipe(int filedis[2]);
当一个管道建立时,它会创建两个文件描述符:
filedis[0]用于读管道,filedis[1]用于写管道。
无名管道用于父子进程间通信。通常先创建一个管道,在通过fork函数创建一个子进程,该子进程会继承父进程所创建的管道描述符(顺序一定要对)。
下面这个是利用无名管道复制文件:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#define SIZE 1024
// 子进程通过管道从父进程接收数据
void child_do(int *fd)
{
// 将管道的写端关闭
close (fd[1]);
int fd_write = open ("2.mmap", O_WRONLY|O_CREAT, 0777);
if (fd_write == -1)
{
perror ("open");
return;
}
int ret;
char buf [SIZE];
// read 从管道读数据,如果管道没有数据可读,read 会阻塞
// 如果 管道的写端 被关闭, read 返回 0
while (ret = read (fd[0], buf, SIZE))
{
if (ret == -1)
{
perror ("read");
break;
}
// 把从父进程接收的数据写入到新文件中
write (fd_write, buf, ret);
}
printf ("文件复制完成\n");
// 关闭读端
close (fd[0]);
close (fd_write);
}
// 父进程通过管道向子进程发送数据
void father_do(int *fd)
{
// 将管道读端关闭
close (fd[0]);
int fd_read = open ("1.mmap", O_RDONLY); //复制文件1.mmap
if (fd_read == -1)
{
perror ("open");
return;
}
int ret;
char buf[SIZE];
while (ret = read (fd_read, buf, SIZE))
{
if (ret == -1)
{
perror ("read");
break;
}
// 把读到的内容发送给子进程
write (fd[1], buf, ret);
}
// 关闭写端
close (fd[1]);
close (fd_read);
}
int main()
{
int fd[2];
// 创建管道
int ret = pipe(fd);
if (ret == -1)
{
perror ("pipe");
return -1;
}
// 创建子进程
pid_t pid = fork();
switch (pid)
{
case -1:
perror ("fork");
break;
case 0: // 子进程
child_do(fd);
break;
default:
father_do(fd);
break;
}
return 0;
}
需要注意的是,如果管道所有的读端都被关闭,继续写数据系统默认的操作是使程序退出。因为会产生一个SIGPIPE信号。
命名管道:
命名管道的管道相当于文件,需要创建在linux文件夹下,是同一系统下不同进程间的通信。既然是文件我们同样可以使用open、write等操作。对的函数mkfifo 下面是个简单的例子:
创建命名管道:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
int ret = mkfifo("/home/mkfifo", 0777);
if (ret == -1)
{
perror ("mkfifo");
return -1;
}
return 0;
}
写入端口:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#define SIZE 1024
int main()
{
int fd = open("/home/mkfifo", O_WRONLY);
if (fd== -1)
{
perror ("mkfifo");
return -1;
}
char buf[SIZE];
while (1)
{
fgets (buf, SIZE, stdin);
write (fd, buf, strlen(buf));
}
return 0;
}
读取端口:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#define SIZE 1024
int main()
{
int fd = open("/home/mkfifo", O_RDONLY);
if (fd == -1)
{
perror ("mkfifo");
return -1;
}
char buf[SIZE];
while (1)
{
int ret = read (fd, buf, SIZE);
buf[ret] = '\0';
printf ("读到 %d 字节: %s\n", ret, buf);
}
return 0;
}
程序运行结果:
三、共享内存
共享内存是被多个进程共享的一部分物理内存。共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。
共享内存的创建步骤:
①创建一个共享内存
②做一下映射内存
③解除映射
④删除共享内存
四个步骤分别用到的函数为shmget,shmat,shmdt,shmctl。函数的用法不再详述,可参考百度https://baike.baidu.com/item/shmget/6875075?fr=aladdin,也可以自己在命令行man一下(一般只需要知道传递的参数和返回值)。下面的例子简单的体现了这四个步骤:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string.h>
typedef struct _shm
{
int flag;
char msg[256];
}SHM;
int main()
{
// 1、创建或者获取一个共享内存
int shmid = shmget((key_t)1234, sizeof(SHM), 0666 | IPC_CREAT);
if (shmid == -1)
{
perror ("shmget");
return -1;
}
// 2、将共享内存映射到当前的进程空间
SHM* pshm = (SHM*)shmat(shmid, NULL, 0);
if(pshm == (SHM*)-1)
{
perror ("shmat");
return -1;
}
strcpy (pshm->msg, "hello"); // 当验证的时候复制此函数将此处修改为printf ("%s\n", pshm->msg);即可。
// 解除共享内存映射,解除是值当前进程不能再使用共享内存
shmdt(pshm);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
四、消息队列
消息队列中的消息的使用时一次性的,读取一次就不能再被读取,但是消息队列是随内核持续,只有在内核重启或者人工删除时,该消息队列才会被删除。
消息队列的内核持续性要求每个消息队列都在系统范围内对应唯一的键值,所以,要获得一个消息队列的描述符,必须提供该消息队列的键值。
消息队列就是一个消息的链表。每个消息队列都有一个队列头,用结构struct msg_queue来描述。队列头中包含了该消息队列的大量信息,包括消息队列键值、用户ID、组ID、消息队列中消息数目等等,甚至记录了最近对消息队列读写进程的ID。读者可以访问这些信息,也可以设置其中的某些信息。
消息队列用到这几个函数:magget(用于创建消息队列),msgsnd(用于写入消息),msgrcv(用于读取一条消息),msgctl(删除)函数的使用方法请参考百度。
https://baike.baidu.com/item/msgget/322046?fr=aladdin,https://baike.baidu.com/item/msgsnd%2Fmsgrcv,https://baike.baidu.com/item/msgctl/715680?fr=aladdin。
下面简单的应用这三个函数:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf
{
long mtype; /* message type, must be > 0 */
char mtext[256]; /* message data */
};
int main()
{
// 创建消息队列
int msgid = msgget((key_t)1234, 0666|IPC_CREAT);
//返回值:与键值key相对应的消息队列的描述符
if (msgid == -1)
{
perror ("msgget");
return -1;
}
struct msgbuf msg;
/*
msg.mtype = 2;
strcpy (msg.mtext, "hello");
int ret = msgsnd(msgid, &msg, 256, IPC_NOWAIT);
if (ret == -1)
{
perror ("nsgsnd");
return -1;
}
*/
/*
int ret = msgrcv(msgid, &msg, 256, 2, IPC_NOWAIT);
if (ret == -1)
{
perror ("nsgsnd");
return -1;
}
printf ("%s\n", msg.mtext);
*/
msgctl(msgid, IPC_RMID, 0);
return 0;
}
文中,第一部分注释的内容是写入消息。第二部分是读出消息。其他地方均相同,请分别放在两个程序中。