进程间相互通信的其他技术-进程间通信
统操作执行命令的时候,经常有需求要将一个程序的输出交给另一个程序进行处理,这种操作可以使用输入输出重定向加文件。如下面的命令:
ubuntu@VM-188-113-ubuntu:~$ ls -l /etc/ > etc.txt
ubuntu@VM-188-113-ubuntu:~$ wc -l etc.txt
179 etc.txt
但是这样未免显得太麻烦了。所以,管道的概念应运而生。目前在任何一个shell中,都可以使用|
连接两个命令,shell会将前后两个进程的输入输出用一个管道相连,以便达到进程间通信的目的:
ubuntu@VM-188-113-ubuntu:~$ ls -l /etc/ | wc -l
179
管道本质上就是一个文件,前面的进程以写方式打开文件,后面的进程以读方式打开。这样前面写完后面读,于是就实现了通信。实际上管道的设计也是遵循UNIX的“一切皆文件”设计原则的,它本质上就是一个文件。Linux系统直接把管道实现成了一种文件系统,借助VFS给应用程序提供操作接口。
虽然实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上,它占用的是内存空间。所以,Linux上的管道就是一个操作方式为文件的内存缓冲区。
管道是UNIX系统IPC的最古老的形式,所有的UNIX系统都提供这一种通信机制。局限性:
管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。
从原理上,管道利用fork机制建立,从而让两个进程可以连接到同一个PIPE上。最开始的时候,上面的两个箭头都连接在同一个进程Process 1上(连接在Process 1上的两个箭头)。当fork复制进程的时候,会将这两个连接也复制到新的进程(Process 2)。随后,每个进程关闭自己不需要的一个连接 (两个黑色的箭头被关闭; Process 1关闭从PIPE来的输入连接,Process 2关闭输出到PIPE的连接),这样,剩下的红色连接就构成了如上图的PIPE。
在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。如下图
有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。
管道是通过pipe
函数创建的
#include <unistd.h>
int pipe(int filedes[2]);
filedes[0]用于读出数据,读取时必须关闭写入端,即close(filedes[1]);
filedes[1]用于写入数据,写入时必须关闭读取端,即close(filedes[0])。
管道读函数pipe_read()及管道写函数pipe_wrtie()。管道写函数通过将字节复制到 VFS 索引节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号。
当写进程向管道中写入时,它利用标准的库函数write(),系统根据库函数传递的文件描述符,可找到该文件的 file 结构。file 结构中指定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。写入函数在向内存中写入数据之前,必须首先检查 VFS 索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作:
如果同时满足上述条件,写入函数首先锁定内存,然后从写进程的地址空间中复制数据到内存。否则,写入进程就休眠在 VFS 索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写入进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接收到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。
管道的读取过程和写入过程类似。但是,进程可以在没有数据或内存被锁定时立即返回错误信息,而不是阻塞该进程,这依赖于文件或管道的打开模式。反之,进程可以休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页也被释放。
一个使用pipe的例子:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define MAXLINE 4096
void ERR_EXIT(const char* m)
{
perror(m);
exit(EXIT_FAILURE);
}
int main()
{
int n;
int fd[2];
pid_t pid;
char line[MAXLINE];
if (pipe(fd) < 0)
ERR_EXIT("pipe error!");
if ((pid = fork()) < 0)
ERR_EXIT("fork error!");
else if (pid > 0)
{
close(fd[0]);
write(fd[1], "hello world\n", 12);
}
else
{
close(fd[1]);
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
}
exit(0);
}
常见的操作是创建一个连接到另一个进程的管道,然后读其输出或者向其输入端发送数据。标准I/O库提供了两个函数popen和pclose。这两个函数实现的操作是:创建一个管道,fork一个子进程,关闭未使用的管道端,执行一个shell运行命令,然后等待命令终止。
#include <stdio.h>
FILE *popen(const char *cmdstring, const char *type);
//返回值:若成功,返回文件指针;若出错,返回NULL
int pclose(FILE *fp);
//返回值:若成功,返回cmdstring的终止状态;若出错,返回-1
UNIX系统过滤程序从标准输入读取数据,对其进行适当处理后写到标准输出。几个过滤程序通常在shell管道命令行中线性地连接。当一个程序产生某个过滤程序的输入,同时又读取该过滤程序的输出时,则该过滤程序就成为协同进程(coprocess)。
进程线创建两个管道:一个是协同进程的标准输入,另一个是协同进程的标准输出。如下图所示:
有3中IPC称为XSI IPC,即消息队列、信号量以及共享存储器,它们之间有很多相似之处。
#include <sys/ipc.h>
key_t ftok(const char *path, int id);
//返回值:若成功则返回键,若出错则返回(key_t)-1
XSI IPC结构设置了一个ipc_perm结构。该结构规定了权限和所有者,至少包括以下成员:
struct ipc_perm {
uid_t uid; /* owner's effective user id */ gid_t gid; /* owner's effective group id */
uid_t cuid; /* creator's effective user id */ gid_t cgid; /* creator's effective group id */
mode_t mode; /* access modes */
...
};
三种形式的XSI IPC都有内置限制(built-in limit)。这些限制的大多数可以通过重新配置内核而加以更改。
消息队列是消息的链接表,存放在内核中并由消息队列标识符标识。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
消息队列的操作有3种类型:
struct mymegs
{
long mtype;//message type
char mtext[BUFFERSIZE];//the length of the message
}
struct msqid_ds
),返回消息队列的属性或者设置消息队列属性。 struct msqid_ds {
struct ipc_perm msg_perm; /* */
msgqnum_t msg_qnum; /* # of messages on queue */
msglen_t msg_qbytes; /* max # of bytes no queue */
pid_t msg_lspid; /* pid of last msgsnd() */
pid_t msg_lrpid; /* pid of last msgrcv() */
time_t msg_stime; /* last-msgsnd() time */
time_t msg_rtime; /* last-msgrcv() time */
time_t msg_ctime; /* last-change time */
...
};
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok (char*pathname, char proj);
#include <sys/msg.h>
int msgget(key_t key, int flag);
返回值:若成功则返回消息队列ID,若出错则返回-1
#include <sys/msg.h>
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
返回值:若成功则返回0,若出错则返回-1
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
返回值:若成功则返回消息的数据部分的长度,若出错则返回-1
msgctl对队列执行多种操作
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
返回值:若成功则返回0,若出错则返回-1
cmd参数说明对由msqid指定的队列要执行的命令:
msgreceive.c接受消息,类型为0,表示接受第一个可用消息。msgsend.c发送消息,发送类型为1.
msgreceive.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/msg.h>
#define BUFSIZE 512
struct msg_st
{
long int msg_type;
char text[BUFSIZE];
};
void err_exit(const char* m)
{
perror(m);
exit(EXIT_FAILURE);
}
int main()
{
int running = 1;
int msgid = -1;
struct msg_st data;
long int msgtype = 0;
//create message queue
msgid = msgget((key_t)1212, 0666 | IPC_CREAT);
if (msgid == -1)
err_exit("create message queue error");
while (running)
{
//receive message
if (msgrcv(msgid, (void*)(&data), BUFSIZE, msgtype, 0) == -1)
err_exit("recieve message error.");
printf("You wrote:%s\n", data.text);
if (strncmp(data.text, "end", 3) == 0)
running = 0;
}
//delete message
if (msgctl(msgid, IPC_RMID, 0) == -1)
err_exit("delete message error.");
exit(0);
};
msgsend.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <error.h>
#include <sys/msg.h>
#define MAX_TEXT 512
struct msg_st
{
long int msg_type;
char text[MAX_TEXT];
};
void err_exit(const char* m)
{
perror(m);
exit(EXIT_FAILURE);
}
int main()
{
int running = 1;
struct msg_st data;
char buffer[MAX_TEXT];
int msgid = -1;
//create message queue
msgid = msgget((key_t)1212, 0666|IPC_CREAT);
if (msgid == -1)
err_exit("create message queue error.");
while (running)
{
printf("Enter some text:\n");
fgets(buffer, MAX_TEXT, stdin);
data.msg_type = 1;
strcpy(data.text, buffer);
if (msgsnd(msgid, (void *)(&data), MAX_TEXT, 0) == -1)
err_exit("send message error.");
if (strncmp(buffer, "end", 3) == 0)
running = 0;
sleep(1);
}
exit(0);
}
运行结果
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch15InterProcessCommunication$ ./msgreveive &
[1] 18484
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch15InterProcessCommunication$ ./msgsend
Enter some text:
test
You wrote:test
Enter some text:
yanke
You wrote:yanke
Enter some text:
end
You wrote:end
[1]+ Done ./msgreveive
信号量是一个计数器,用于多个进程对数据对象共享访问。为了获得共享资源,进程需要执行的操作为:
当进程不再使用由一个信号量控制的共享资源时,该信号量值增1。如果有进程正在休眠等待此信号量,则唤醒它们。为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。
内核为每个信号量维护的数据结构semid_ds
struct semid_ds {
struct ipc_perm sem_perm;
unsigned short sem_nsems; /* # of semaphores in set */
time_t sem_otime; /* last-semop() time */
time_t sem_ctime; /* last-change time */
...
};
每个信号量都有一个无名的表示,它至少包含一下成员:
struct {
unsigned short semval; /* semaphore value, always >= 0 */
pid_t sempid; /* pid for last operation */
unsigned short semncnt; /* # processes awaiting semval>curval */
unsigned short semzcnt; /* # processes awaiting semval==0 */
};
在Linux系统中使用信号量的四个步骤
创建或使用一个信号量
#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);
返回值:若成功则返回信号量ID,若出错则返回-1
semctl函数包含了多种信号量操作
#include < sys/sem.h>
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);
cmd:
arg是一个union,这是一个联合,而非指向联合的指针。
union semun {
int val; /* for SETVAL */
struct semid_ds *buf; /* for IPC_STAT and IPC_SET */
unsigned short *array; /* for GETALL and SETALL */
};
函数semop自动执行信号量集合上的操作数组,这是个原子操作。
#include <sys/sem.h>
int semop(int semid, struct sembuf semoparray[], size_t nops);
返回值:若成功则返回0,若出错则返回-1
struct sembuf {
unsigned short sem_num; /* member # in set ( 0, 1, ..., nsems-1) */
short sem_op; /* operation (negtive, 0, or positive) */
short sem_flag; /* IPC_NOWAIT, SEM_UNDO */
};
使用信号量的例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/sem.h>
union semun
{
int val;
struct semid_ds* buf;
unsigned short* array;
};
static int sem_id = 0;
void err_exit(const char* m)
{
perror(m);
exit(EXIT_FAILURE);
}
int set_semvalue()
{
//init semaphores
union semun sem_union;
sem_union.val = 1;
if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
//err_exit("init sempaores error!");
return 0;
return 1;
}
void del_semvalue()
{
union semun sem_union;
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
err_exit("delete semaphores error!");
}
int sempahore_p()
{
//p operator
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1)
// err_exit("p operator error!");
return 0;
return 1;
}
int semaphore_v()
{
//v operator
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1)
//err_exit("v operator error!");
return 0;
return 1;
}
int main(int argc, char** argv)
{
char message = 'x';
int i = 0;
//create semaphore
sem_id = semget((key_t)1212, 1, 0666 | IPC_CREAT );
if (argc > 1)
{
//first use semaphore
if (!set_semvalue())
err_exit("init semaphore error!");
message = argv[1][0];
sleep(2);
}
for (i = 0; i < 10; ++i)
{
//进入临界区
if(!sempahore_p())
err_exit("p operator error!");
printf("%c ", message);
fflush(stdout);
sleep(rand() % 3);
printf("%c ", message);
fflush(stdout);
if (!semaphore_v())
err_exit("p operator error!");
sleep(rand() % 2);
}
sleep(10);
printf("\n %d - finished", getpid());
if (argc > 1)
{
sleep(3);
del_semvalue();
}
exit(0);
}
输出:
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch15InterProcessCommunication$ ./seml y & ./seml [1] 22522
x x x x y y x x y y x x y y x x x x y y x x y y x x y y x x x x y y y y y y y y
22523 - finishedubuntu@VM-188-113-ubuntu:~/Code/apue/ch15InterProcessCommunication$
22522 - finished
[1]+ Done ./seml y
XSI共享存储和内存映射的文件不同之处在于,前者没有相关的文件,XSI共享存储段的内存的匿名段。内核维护的共享段的数据结构:
struct shmid_ds {
struct ipc_perm shm_perm;
size_t shm_segsz; /* size of segment in bytes */
pid_t shm_lpid; /* pid of last shmop() */
pid_t shm_cpid; /* pid of creator */
shmatt_t shm_nattch; /* number of current attaches */
time_t shm_atime; /* last-attach time */
time_t shm_dtime; /* last-detach tiime */
time_t shm_ctime; /* last-change time */
...
};
shmatt_t类型定义为不带符号整型,它至少与unsigned short一样大。
获得一个共享内存标志符
#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag);
返回值:若成功则返回共享存储ID,若出错则返回-1
对共享内存段执行多种操作
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
返回值:若成功则返回0,若出错则返回-1
一旦创建了一个共享存储段,进程就可调用shmat将其连接到它的地址空间中。
#include <sys/shm.h>
void *shmat(int shmid, const void *addr, int flag);
返回值:若成功则返回指向共享存储的指针,若出错则返回-1
共享存储段连接到调用进程的哪个地址上与addr参数以及在flag中是否指定SHM_RND位有关。
当对共享存储段的操作已经结束时,则调用shmdt脱接该段。
#include <sys/shm.h>
int shmdt(void *addr);
返回值:若成功则返回0,若出错则返回-1
shmdata.h
#ifndef __SHM_DATA_H__
#define __SHM_DATA_H__
#include <stdio.h>
#include <stdlib.h>
#define TEXT_SZ 2048
struct shared_use_st
{
int written;//flag:0 writeable, others readable
char text[TEXT_SZ];//record text
};
void err_exit(const char* m)
{
perror(m);
exit(EXIT_FAILURE);
}
#endif
shmread.c
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/shm.h>
#include "shmdata.h"
int main()
{
int running = 1;
void* shm = NULL; //address of the shared memory
struct shared_use_st *shared;
int shmid;
//create shared memory
shmid = shmget((key_t)1212, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
if (shmid == -1)
err_exit("create shared memory error!");
//attach address to the process
shm = shmat(shmid, 0, 0);
if (shm == (void*)-1)
err_exit("shmat error.");
printf("\n Memory attached at %X\n", (int)shm);
//set shared memory
shared = (struct shared_use_st*)shm;
shared->written = 0;//fist write
while (running)
{
//read process
if (shared->written != 0)
{
//print the text
printf("You wrote : %s\n", shared->text);
sleep(rand() % 3);
//then set shared writable
shared->written = 0;
if (strncmp(shared->text, "end", 3) == 0)
running = 0;
}
else// other process is writing text
sleep(1);
}
if (shmdt(shm) == -1)
err_exit("shmdt error.");
if (shmctl(shmid, IPC_RMID, 0) == -1)
err_exit("shctl failed!");
exit(0);
}
shmwrite.h
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include "shmdata.h"
int main()
{
int running = 1;
void* shm = NULL;
struct shared_use_st* shared = NULL;
char buffer[BUFSIZ];//the text
int shmid;
//create
shmid = shmget((key_t)1212, sizeof(struct shared_use_st), 0666|IPC_CREAT);
if (shmid == -1)
err_exit("shmget error!");
shm = shmat(shmid, (void*)0,0);
if (shm == (void*)-1)
err_exit("shmat error!");
printf("memory attached at %x\n", (int)shm);
shared = (struct shared_use_st*)shm;
while(running)
{
while (shared->written == 1)
{
sleep(1);
printf("Waiting ...\n");
}
printf("Enter some text:\n");
fgets(buffer, BUFSIZ, stdin);
strncpy(shared->text, buffer,TEXT_SZ);
shared->written = 1;
if (strncmp(buffer, "end", 3) == 0)
running = 0;
}
if (shmdt(shm) == -1)
err_exit("shmdt failed!");
sleep(2);
exit(0);
}
输出:
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch15InterProcessCommunication$ ./shmread &
[1] 30889
ubuntu@VM-188-113-ubuntu:~/Code/apue/ch15InterProcessCommunication$
Memory attached at 2EC64000
./shmwrite
memory attached at e66cd000
Enter some text:
test
You wrote : test
Waiting ...
Waiting ...
Enter some text:
yanke
You wrote : yanke
Waiting ...
Waiting ...
Enter some text:
end
You wrote : end
[1]+ Done ./shmread
POSIX信号量相比于XSI信号量:
POSIX信号量有两种:有名信号量和无名信号量,无名信号量也被称作基于内存的信号量。有名信号量通过IPC名字进行进程间的同步,而无名信号量如果不是放在进程间的共享内存区中,是不能用来进行进程间同步的,只能用来进行线程同步。
(1)创建一个信号量:
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, ... /* mode_t mode, unsigned int value */ );
返回值:若成功则返回指向信号量的指针,若出错则返回SEM_FAILED
(2)释放与信号量相关的资源
#include <semaphore.h>
int sem_close(sem_t *sem);
返回值:若成功则返回0,出错返回-1
(3)销毁一个有名信号量
#include <semaphore.h>
int sem_unlink(const char *name);
返回值:若成功则返回0,出错则返回-1
(4)请求一个信号量:
如果信号量计数为0,这时如果调用sem_wait函数,将会阻塞。直到成功对信号量计数减1或被一个信号中断,sem_wait函数才会返回。我们可以使用sem_trywait函数以避免阻塞。当我们调用sem_trywait函数时,如果信号量计数为0,sem_trywait会返回-1,并将errno设置为EAGAIN。
#include <semaphore.h>
int sem_trywait(sem_t *sem);
int sem_wait(sem_t *sem);
两个函数返回值:若成功则返回0,出错则返回-1
使用sem_timedwait函数:tsptr参数指定了希望等待的绝对时间。如果信号量可以被立即减1,那么超时也无所谓,即使你指定了一个已经过去的时间,试图对信号量减1的操作也会成功。如果直到超时,还不能对信号量计数减1,那么sem_timedwait函数将会返回-1,并将errno设置为ETIMEDOUT。
#include <semaphore.h>
#include <time.h>
int sem_timedwait(sem_t *restrict sem, const struct timespec *restrict tsptr);
返回值:若成功则返回0,出错则返回-1
(5)信号量值加1。这类似于对一个二值信号量解锁或释放一个与计数信号量有关的资源。
#include <semaphore.h>
int sem_post(sem_t *sem);
返回值:若成功则返回0,出错则返回-1
(6) 调用sem_init函数创建一个无名信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
返回值:若成功则返回0,出错返回-1
(7) 调用sem_destroy函数来销毁用完的无名信号量
#include <semaphore.h>
int sem_destroy(sem_t *sem);
返回值:若成功则返回0,出错则返回-1
(8) 调用sem_getvalue函数来获取信号量值。
#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *restrict valp);
返回值:若成功则返回0,出错则返回-1
[1] http://liwei.life/2016/07/18/pipe/
[2] http://www.cnblogs.com/biyeymyhjob/archive/2012/11/03/2751593.html
[3] http://blog.csdn.net/jiajiayouba/article/details/8815042
[4] http://blog.csdn.net/ljianhui/article/details/10287879
[5] http://blog.csdn.net/mybelief321/article/details/9086151
[6] http://blog.csdn.net/ljianhui/article/details/10243617
[7] http://blog.csdn.net/ljianhui/article/details/10253345