在Linux中,进程间通信的方法有多种,像管道,FIFO,共享内存,信号灯还有消息队列。
管道:
在此介绍一下有名管道和无名管道两种,有名管道通常称为FIFO,他存在于文件系统中,无名管道没有名字因为他们从来没有路径名,也不还会在文件系统中出现,严格的说,无名管道是和内存中的一个索引节点相关联的两个文件描述符,最后的进程关闭了这些文件描述符中的一个,导致索引节点,也就是管道消失,有名管道和无名管道都是半双工的,也就是说,数据只能由一边流向令一边,但不能反过来,另外他们也不能够用像lseek类似的函数来进行文件的定位。说明一下,下面如果没有特殊说明,管道说的是无名管道,FIFO说的是有名管道。
一,打开和关闭管道
#include <unistd.h>
int pipe(int filedes[2]);
在准备向管道中写数据,或者从管道中读数据的时候,这个管道必须是存在的,如果pipe函数打开管道成功,则会打开两个文件描述符,并把他们保存在filedes数组中,其中,filedes[0]用于读数据,filedes[1]用来写数据。函数执行成功则会返回0,否则返回-1,并设置errno变量。值得强调的是。这里的文件描述符不能和一个磁盘文件相对应,他只是主留在内存的一个索引节点。
要关闭一个管道,只要调用close函数,正常关闭文件描述符。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
int fdp[2];
if (0 > pipe(fdp)) /* open a pipe. */
{
perror("creat pipe error\n");
exit(EXIT_FAILURE);
}
printf("We get a pipe :\nRead : %d\nWrite : %d\n",
fdp[0], fdp[1]);
close(fdp[0]); /* close the pipe. */
close(fdp[1]);
return 0;
}
二,读写管道
由于在使用pipe函数打开的是两个文件描述符,所以,对管道的读写操作,仅需要用对文件描述符读写的read函数从filedes[0]中读数据和write函数从filedes[1]中写数据,一般的规则是,读数据的进程关闭管道的写入端,写数据的进程关闭管道的读出端。注意,在关闭管道的写入端后,从管道读数据会返回0以示文件末尾,如果关闭读取端,任何写入管道的尝试都会给写入方产生SIGPIPE信号,而write返回-1,并设置errno为EPIPE,如果写入方不能捕获或者忽略SIGPIPE信号,则写入进程会中断。
如果多个进程向同一个管道写入数据,则每个进程写入的数据必须少于PIPE_BUF个字节,PIPE_BUF被定在limits.h中,以保证每个进程每次向管道中写数据都是一个原子操作,而不会发生数据混乱的现象。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
int main(void)
{
int prcw[2]; /* parent read, child write. */
int pwcr[2]; /* parent write, child read. */
pid_t pid;
if (0 > pipe(pwcr) || 0 > pipe(prcw))
{
perror("Open pipe error\n");
exit(EXIT_FAILURE);
}
if (0 > (pid = fork()))
{
perror("creat child error\n");
exit(EXIT_FAILURE);
}
if (0 == pid)
{
char buf[100];
close(prcw[0]); /* send message to parent. */
write(prcw[1], "Child sent to parent.", sizeof(buf));
close(prcw[1]);
close(pwcr[1]); /* read message from parent. */
read(pwcr[0], buf, 100);
printf("Child read : %s\n", buf);
close(pwcr[0]);
}
else
{
char buf[100];
close(pwcr[0]); /* sent message to child. */
write(pwcr[1], "Parent sent to child.", sizeof(buf));
close(pwcr[1]);
close(prcw[1]); /* read message from child. */
read(prcw[0], buf, 100);
printf("Parent read : %s\n", buf);
close(prcw[0]);
}
waitpid(pid, NULL, 0);
return 0;
}
三,标准库中的管道相关函数
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
popen函数首先创建一个管道,然后fork出一个子进程,然后调用exec,调用/bin/sh -c执行保存在command中的命令,mode的值必须为r或者w。如果为r,那么popen返回的文件指针用来读数据,并且,从这个文件中读出的数据,和command执行的输出是一样的。同样,如果为w,那么,对popen函数返回的指针的些操作就是command执行的输入。如果popen函数失败,函数返回NULL,并设置errno变量。
使用pclose来关闭I/O流,他会等待command完成,并向调用进程返回他的退出状态,如果调用失败,pclose返回-1。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char * com = "ps";
char buf[1024] = {0};
FILE * pi;
if (NULL == (pi = popen(com, "r"))) /*creat it. */
{
perror("popen failed\n");
exit(EXIT_FAILURE);
}
fread(buf, 1024, 1, pi); /* read from pipe */
printf("%s\n", buf);
pclose(pi);
return 0;
}
FIFO,有名管道
一,创建FIFO
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
函数mkfifo用来建立一个名为pathname的FIFO,mode用来指出该FIFO文件文件的权限,通常,mode的值会被程序中的umask修改。如果执行成功,函数返回0,失败返回-1,并设置errno变量。
二,操作FIFO
对FIFO文件的打开,关闭,删除,读写,与普通文件一样,使用open,close,unlink,read,write来完成。有几点需要注意,FIFO的两端在使用之前都必须打开,如果以O_NONBLOCK和O_RDONLY打开,那么调用会立即返回。如果以O_NONBLOCK和O_WRONLY打开时,调用open会返回一个错误,并把errno设置为ENXIO。另外如果没有指定O_NONBLOCK,那么以O_RDONLY打开时,open会被阻塞,直到另一个进程为写入数据打开FIFO为止。O_WRONLY打开时也会被阻塞到为读数据打开FIFO为止。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
int readfifo;
int len;
char buf[1024] = {0};
if (0 > mkfifo("tmp", 0666)) /* creat fifo. */
{
perror("Make FIFO error\n");
exit(EXIT_FAILURE);
}
if (0 > (readfifo = open("tmp", O_RDONLY))) /* open FIFO. */
{
perror("Read FIFO error\n");
exit(EXIT_FAILURE);
}
puts("We are in ReadFIFO\n");
/* read FIFO. */
while (0 < (len = read(readfifo, buf, 1024)))
{
printf("Test ");
printf("%s\n",buf);
}
printf("%d",len);
close(readfifo); /* close FIFO. */
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(void)
{
int len;
int wr;
char buf[1024] = {0};
if (0 > (wr = open("tmp", O_WRONLY))) /* open FIFO. */
{
perror("Open FIFO error\n");
exit(EXIT_FAILURE);
}
/* write to FIFO. */
while (1)
{
len = sprintf(buf, "%5d : ---\n", getpid());
if (0> write(wr, buf, len + 1))
{
perror("Write error\n");
close(wr);
exit(EXIT_FAILURE);
}
puts(buf);
sleep(3);
}
close(wr);
return 0;
}
System V IPC概述
IPC存在于系统内核,而不是文件系统中,他让其他无关进程通过一种IPC机制相互进行通信,数据在使用IPC机制的进程间自由流动。每个对象通过他的标识符来引用和访问,标识符是一个正整数,他唯一的标识出对象本身和他的类型,每个标识符的类型都是唯一的,但是同一标识符的值可以用于一个消息队列,一个信号灯和一个共享内存区。标识符成为该结构上所有其他操作的句柄。
每个IPC结构都由get函数创建,semget用来创建信号灯,msgget用来创建消息队列,shmget用来创建共享内存,每次调用的时候必须提供一个关键字,在创建一个IPC之后,使用同一关键字的get函数不会再创建新结构,但会返回和现有结构相关的标识符。这样就可以使两个或两个以上的进程之间建立IPC通道。
共享内存
共享内存是内核出于在多个进程间交换信息的目的而预留出的一块内存区(段),如果段的权限设置的恰当,每个进程都能够将其映射到自己的私有空间中。如果其中一个进程改变了这会共享内存中的内容,那么,所有其他的进程将都会看到。
一,创建共享内存区
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
其中,shmflg可以是一个或多个IPC_CREAT,IPC_EXCL和一组权限位按位或的结果。权限位以八进制表示,IPC_EXCL在段已经存在的情况下,调用将会失败而不会返回一个已经存在的标识符。size用来指定大小,但他以PAGE_SIZE为界.
#include <sys/shm.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int shmid;
if (0 > (shmid = shmget(IPC_PRIVATE, 2048, 0666)))
{
perror("Share memery faied\n");
exit(EXIT_FAILURE);
}
printf("share memery id : %d\n", shmid);
system("ipcs -m");
return 0;
}
二,附加共享内存
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
建立完一块共享内存之后,进程是不能马上使用的,必须先将其附加到该进程,同样,当共享内存使用完毕之后,还要将起与进程分离。其中在附加共享内存时,shmaddr通常为0,这样内核会把段映像到调用的进程的地址空间中他所选定的位置。shmflg可以为SHM_RDONLY,表示只读,否则,被附加的段默认是可读可写的。如果调用成功shmat将返回段地址,否则返回-1,并设置errno变量。shmdt用来将段从进程中分离出去,其中参数shmaddr必须是shmat所返回的地址。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(int argc, char * argv[])
{
int id = atoi(argv[1]);
char * shm;
if ((char *)0 > (shm = shmat(id, 0, 0)))
{
perror("Failed on shmat\n");
exit(EXIT_FAILURE);
}
printf("Shared %p\n", shm);
system("ipcs -m");
getchar();
if (0 > shmdt(shm))
{
perror("Failed on shmed\n");
exit(EXIT_FAILURE);
}
system("ipcs -m");
return 0;
}
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>
void error(const char * msg);
int main(int argc, char * argv[])
{
int shmid = atoi(argv[1]);
char * shm;
int i = 0;
if ((char *)0 > (shm = shmat(shmid, 0, 0)))
error("failed on shmat\n");
printf("Share memery : %p\n", shm);
if (NULL == (shm = malloc(1024))) /* I don't understand why it
malloc. and we do not get
the shared memery from shm.*/
error("failed on malloc");
printf("Get memery : %p\n", shm);
while (i < 26)
{
shm[i] = 'a' + i;
i++;
}
shm[i] = '\0';
printf("%s\n",shm);
free(shm);
// shmdt(shm); /* is it right after free it? */
/* and is it safe? */
return 0;
}
void error(const char * msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
消息队列
消息队列是一个信息的链接列表,消息都保存在内核中。如果你向队列中添加一条消息,那么新的消息将会被加到队列的尾部,但与FIFO不同的是,你可以随意的检索队列中的消息。
一,创建和打开消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
如果msgflg的值为IPC_PRIVATE,则会创建一个新的消息队列,如果不是,并且key的值与当前队列中的值都不相同,并且在msgflg中设置了IPC_CREAT位,那么也将会创建一个新的队列,否则将会返回和key有关的已有队列的ID。如果执行失败,函数返回-1,并设置errno。
值得注意的是,在创建System V IPC对象的时候,umask不会修改访问权限,也就是或,如果不在创建的时候指明权限的话,就会采用默认的权限0,那么,到时候即使创建者也会无权读写该对象。
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/msg.h>
int main(void)
{
int id;
key_t key = 1111;
if (0 > (id = msgget(key, IPC_CREAT | 0666)))
{
perror("failed on msgget\n");
exit(EXIT_FAILURE);
}
printf("Queue id : %d\n", id);
if (0 > (id = msgget(key, 0)))
{
perror("failed on second \n");
exit(EXIT_FAILURE);
}
printf("Queue id : %d\n", id);
return 0;
}
二,写入消息
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
函数msgsnd会将一条新的消息添加到队列的末尾。如果成功将会返回0,否则返回-1,并设置errno。其中,msqid必须是msget返回的队列ID,msgflg不是0,就是IPC_NOWAIT如果为0,那么当系统内消息总数达到最大时,将会被阻塞,直到一条消息被删除或读取,而IPC_NOWAIT则是在系统最大数时不等待,直接返回错误值。msgsz是你的消息的长度。msgp是指向msgbuf的指针,其定义如下:
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
这个结构声明实际上只是一个模板,你可以对其进行扩展来使其满足你的需求。例如
struct msgbuf {
long mtype;
int i;
char c[10];
};
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct msg {
long type;
char msgbuf[1024];
};
int main(int argc, char * argv[])
{
int qid = atoi(argv[1]);
int len = 0;
struct msg msgb;
puts("Input message");
if (NULL == fgets(msgb.msgbuf, 1024, stdin))
{
strcpy(msgb.msgbuf, "test...");
}
msgb.type = 123;
len = strlen(msgb.msgbuf);
if (0 > msgsnd(qid, &msgb, len, 0))
{
perror("Failed on msgsnd\n");
exit(EXIT_FAILURE);
}
puts("message posted");
return 0;
}
三,读取消息
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msgid用来指明队列ID,msgp指向得到消息后要填充的结构体,msgsz指明大小,msgty与前面说明的结构体中对应的mtype,如果他是0,则返回队列中的第一条消息,大于0则返回等于msgtyp的消息,小于0则返回msgtyp小于等于mtype的第一条消息。msgflg个控制msgrcv的行为,如果msgflg的值为MSG_NOERROR那么,如果消息返回的字节比msgsz多,消息就会被截短,否则返回-1,并设置errno为E2BIG,消息仍然会保存在队列中。如果是IPC_NOWAIT,如果没有指定类型的消息那么就直接返回,并设置errno为ENOMSG,否则megrsv将会被阻塞知道出现匹配的消息。如果函数执行成功将会从消息队列中删除返回的消息。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/ipc.h>
struct msg {
long type;
char msgbuf[1024];
};
int main(int argc, char * argv[])
{
int id = atoi(argv[1]);
struct msg msgb;
int len;
len = msgrcv(id, &msgb, 1024, 0, 0);
msgb.msgbuf[len] = '\0';
puts("Get message :");
puts(msgb.msgbuf);
return 0;
}
四,控制消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
其中cmd的取值可以是:
IPC_RMID : 删除队列msqid
IPC_STAT : 用队列的msqid_ds结构填充buf,并让你查看队列的内容,但不会删除任何消息,这是一种非破怀性的读。
struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions*/
time_t msg_stime; /* Time of last msgsnd() */
time_t msg_rtime; /* Time of last msgrcv() */
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Current number of bytes in
queue (non-standard) */
msgqnum_t msg_qnum; /* Current number of messages
in queue */
msglen_t msg_qbytes; /* Maximum number of bytes
allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd() */
pid_t msg_lrpid; /* PID of last msgrcv() */
};
IPC_SET : 让你改变队列的UID,GID,访问模式和队列的最大字节数。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main(int argc, char * argv[])
{
int qid = atoi(argv[1]);
if (0 > msgctl(qid, IPC_RMID, NULL))
{
perror("failed on msgctl\n");
exit(EXIT_FAILURE);
}
printf("Delete %d successed\n", qid);
return 0;
}
信号灯
在这里只讨论最简单的信号灯,即双态信号灯,一个双态信号灯的值只能取两个值中的一个,即当资源被加锁不能被其他进程访问时为0,而当资源解锁时为1。
一,创建信号灯
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
在使用一个信号灯之前必须先创建一个信号灯,semget返回一个和nsems信号灯集合相关的信号灯标识符。如果key为IPC_PRIVATE或者key还没有使用,在semflg中设置了IPC_CREAT位,则会创建新的信号灯集合。如果出错函数返回-1,并设置errno,成功返回和key值相关联的信号灯标识符。
二,使用信号灯
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
semid是先前用semget返回的信号灯的标识符,他指向要操作的信号灯的集合。nsops是sops指向的sembuf结构数组的元素的个数,
struct sembuf {
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
}
其中sem_op是执行的操作:
> 0 : 信号灯控制的资源被释放,而且信号灯的值被增加。
< 0 : 调用进程将等待直到受控资源被释放,此时信号灯的值减小,而资源被调用进程加锁。
= 0 : 调用进程阻塞直到信号灯变为0,如果信号灯已经为0,调用立即返回。
sem_flg的值可以IPC_NOWAIT或者SEM_UNDO,后者意味着当调用semop的进程退出后执行的操作将被撤销。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int semid;
int nsems = 1;
int flags = 0666;
struct sembuf buf;
if (0 > (semid = semget(IPC_PRIVATE, nsems, flags)))
{
perror("failed on semget\n");
exit(EXIT_FAILURE);
}
printf("semid = %d\n", semid);
system("ipcs -s");
buf.sem_num = 0;
buf.sem_op = 1;
buf.sem_flg = IPC_NOWAIT;
if (0 > semop(semid, &buf, nsems))
{
perror("failed on semop\n");
exit(EXIT_FAILURE);
}
system("ipcs -s");
return 0;
}
三,控制信号灯
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
cmd的取值如下:
GETVAL 返回信号灯当前状态(解锁或解锁)
SETVAL 设置信号灯的状态为arg.val
GETPID 返回上次调用semop进程的PID
GETNCNT 返回在信号灯上等待的进程数
GETZCNT 返回等待信号灯值为0的进程数
GETALL 返回和semid相关联的集合中的所有信号灯的进程数
SETALL 设置和semid相关联的集合中所有信号灯值为保存在arg.array中的值
IPC_RMID 删除带有semid的信号灯
IPC_SET 在信号灯上设置模式
IPC_STAT 复制配置信息
如果调用失败,返回-1,并设置errno
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux specific) */
};
这是semctl函数的第四个参数的模板。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int main(int argc, char * argv[])
{
int id = atoi(argv[1]);
if (0 > semctl(id, 0, IPC_RMID))
{
perror("failed on semctl\n");
exit(EXIT_FAILURE);
}
printf("Delete %d success!\n", id);
system("ipcs -s");
return 0;
}