大纲
每个进程有各自不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到。所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间中拷贝到缓冲区,进程2再从缓冲区把数据读走。内核提供的这种机制就是进程间通信。通信需要媒介,两个进程间通信的媒介就是内存。通信的原理就是让两个或多个进程能够看到同一块共同的资源,这块资源一般都是由内存提供。
管道
管道是IPC最基本的一种实现机制。我们都知道在Linux下“一切皆文件”,其实这里的管道就是一个文件。管道实现进程通信就是让两个进程都能访问该文件。
匿名管道
- 只提供单向通信,也就是说,两个进程都能访问这个文件,假设进程1往文件内写东西,那么进程2 就只能读取文件的内容。
- 只能用于具有血缘关系的进程间通信,通常用于父子进程建通信
- 依赖于文件系统,它的生命周期随进程的结束结束(随进程)
int pipe(int pipefd[2]);
调用pipe函数时,首先在内核中开辟一块缓冲区用于通信,它有一个读端和一个写端,然后通过pipefd参数传出给用户进程两个文件描述符,pipefd[0]指向管道的读端,pipefd[1]指向管道的写段。在用户层面看来,打开管道就是打开了一个文件,通过read()或者write()向文件内读写数据,读写数据的实质也就是往内核缓冲区读写数据。
返回值:成功返回0,失败返回-1。
由于只能用于具有血缘关系的进程间通信,因此在这里我们可以调用fork函数,创建一个子进程,子进程就也会有两个文件描述符指向管道两端,两个进程都可以作为写或者读端。注意:如果一方选择读或写,要记得关掉另一端
使用样例
写一个子读父写的程序
int main()
{
int pd[2];
if (pipe(pd) < 0) {}
int pid = fork();
if (pid < 0) {}
char buf[BUFSIZE];
if (pid == 0)
{
close(pd[1]); //子进程用pd[0]就关掉pd[1]
int len = read(pd[0], buf, BUFSIZE);
write(1, buf, len);
close(pd[0]); //用完要记得关掉
exit(0);
}
else if (pid > 0)
{
close(pd[0]); //父进程用pd[1]就关掉pd[0]
write(pd[1], "HelloWorld", 10);
close(pd[1]);
wait(NULL);
}
exit(0);
}
命名管道
上述管道虽然实现了进程间通信,但是它具有一定的局限性:首先,这个管道只能是具有血缘关系的进程之间通信;第二,它只能实现一个进程写另一个进程读,而如果需要两者同时进行时,就得重新打开一个管道。
为了使任意两个进程之间能够通信,就提出了命名管道FIFO。使用mkfifo函数创建
int mkfifo(const char *pathname, mode_t mode);
参数pathname指出想要创建的FIFO文件所在路径,参数mode指定创建的这个管道的访问模式(通过mode % ~umask实现)
与匿名管道不同的是:
- 提供了一个路径名与之关联,以FIFO文件的形式存储于文件系统中,能够实现任何两个进程之间通信。而匿名管道对于文件系统是不可见的,它仅限于在父子进程之间的通信。
- FIFO是一个设备文件,在文件系统中以文件名的形式存在,因此即使进程与创建FIFO的进程不存在血缘关系也依然可以通信,前提是可以访问该路径。
- FIFO遵循先进先出的原则,即第一个进来的数据会第一个被读走。
XSI IPC
有三种通信机制:消息队列、信号量数组、共享内存,因为有很多相似之处,所以统一称它们为XSI IPC。它们三个有以下相似之处
- 标识符和键
每个XSI IPC都用一个非负整数的标识符加以引用。比如对一个消息队列发送或取消消息,只要知道其队列标识符即可。有点像文件标识符,但又不完全一样,文件描述符总是找当前系统中可用的最小的数,而ipc表示是持续加1的,直到达到整数最大值,然后回转到0。
标识符是IPC对象的内部名,两个进程间的这个标识符可能会不一样。为使多个合作进程能够在同一IPC对象上会合,需要提供一个外部名方案。为此使用了键(key),每个IPC对象都与一个键相关联,于是键就用为该对象的外部名。使用ftok函数来创建一个键
key_t ftok(const char *pathname, int proj_id);
//其中参数fname是指定的文件名,这个文件必须是存在的而且可以访问的
key就相当于一个用于通信的中间介质的标识。
- 权限结构
XSI IPC为每一个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 */
...
};
在创建IPC结构时,对所有字段都赋初值。以后,可以调用msgctl、semctl或shmctl修改uid、gid和mode字段。为了改变这些值,调用进程必须是IPC结构的创建者或超级用户。更改这些字段类似于对文件调用chown和chmod。
三个通信机制都有类似的三个操作函数,形式:xxxget用于创建,xxxop用于使用,xxxctl用于控制。其中xxx用于消息队列就是msg、用于信号量数组就是sem、用于共享内存就是shm。
消息队列message queue
消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
msgget
创建和访问一个消息队列
int msgget(key_t key, int msgflg);
//成功则返回消息队列的ID值,否则返回-1
key如果为IPC_PRIVATE,就会创建一个只有创建者进程才可以访问的ipc对象。通常用于父子进程之间。key值本身就是用于给两个没有亲缘关系的进程操作同一个ipc对象,而现在是两个有亲缘关系的进程,所以就不需要在get函数(包括semget和shmget)之前ftok获得key值了。
非0的key表示创建一个可以被多个进程共享的信号量。
msgflg是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg可以与IPC_CREAT做或操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被忽略,而只返回一个标识符。
msgsnd
该函数用来把消息添加到消息队列中
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//如果调用成功,消息数据的一分副本将被放到消息队列中,并返回0,失败时返回-1.
msgid是由msgget函数返回的消息队列标识符。
msg_ptr是一个指向准备发送消息的指针,但是消息的数据结构却有一定的要求,指针msg_ptr所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定消息的类型。所以消息结构要定义成这样:
struct my_message {
long message_type;
/* The data you wish to transfer */
};
msgsz 是msgp指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说msgsz是sizeof(struct my_message) - sizeof(long);
msgflg 用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情
msgrcv
从一个消息队列获取消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
int msgflg);
前三个参数作用类似于msgsnd。msgtype 可以实现一种简单的接收优先级。如果msgtype为0,就获取队列中的第一个消息。如果它的值大于零,将获取具有相同消息类型的第一个信息。如果它小于零,就获取类型等于或小于msgtype的绝对值的第一个消息。
msgflg 用于控制当队列中没有相应类型的消息可以接收时将发生的事情。
调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的缓存区中,然后删除消息队列中的对应消息。失败时返回-1。
msgctl
对消息队列msqid进行cmd操作,buf是想传入的参数
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
//成功时返回0,失败时返回-1
cmd是将要采取的动作,它可以取3个值,
- IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。
- IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值
- IPC_RMID:删除消息队列
buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。msgid_ds结构至少包括以下成员:
struct msgid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
使用样例
//定义消息的结构/协议
//proto.h
#define KEYPATH "/etc/services"
#define KEYPROJ 'g'
#define NAMESIZE 32
struct msg_st
{
long mtype;
char name[NAMESIZE];
int math;
int chinese;
};
//接收方
//rcver.c
int main()
{
key_t key = ftok(KEYPATH, KEYPROJ);
if (key < 0) {}
//接收方一般先于发送方运行,所以要起到创建消息队列的作用
int msgid = msgget(key, IPC_CREAT | 0600);
if (msgid < 0) {}
struct msg_st rbuf;
//不停的接受打印
while (1)
{
if (msgrcv(msgid, &rbuf, sizeof(rbuf) - sizeof(long), 0, 0) < 0)
{
perror("msgrcv()");
exit(1);
}
printf("NAME = %s\n", rbuf.name);
printf("MATH = %d\n", rbuf.math);
printf("CHINESE = %d\n", rbuf.chinese);
}
msgctl(msgid, IPC_RMID, NULL); //最后删除消息队列
exit(0);
}
//发送方
//snder.c
int main()
{
key_t key = ftok(KEYPATH, KEYPROJ);
if (key < 0) {}
int msgid = msgget(key, 0);
if (msgid < 0) {}
struct msg_st sbuf;
sbuf.mtype = 1;
strcpy(sbuf.name, "Alan");
sbuf.math = 99;
sbuf.chinese = 60;
if (msgsnd(msgid, &sbuf, sizeof(sbuf) - sizeof(long), 0) < 0)
{
perror("msgsnd()");
exit(1);
}
puts("send");
exit(0);
}
信号量
信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行资源申请(即P)和资源释放(即V)信息操作。
信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
- P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
- V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1
semget
创建一个新信号量或取得一个已有信号量
int semget(key_t key, int nsems, int semflg);
//成功返回一个相应信号标识符(非零),失败返回-1
第二个参数nsems指定需要使用的信号量数目,如果是创建新集合,则必须制定nsems。如果引用一个现存的集合,则将nsems指定为0。
第三个参数semflg是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
semop
改变信号量的状态
int semop(int semid, struct sembuf *sops, size_t nsops);
sem_id就是semget()返回的信号量标识。
第二个参数是结构体数组的起始地址,nsops则是数组中的元素数。sembuf结构的定义如下:
struct sembuf{
short sem_num; // 操作信号在信号集中的编号,第一个信号的编号是0
short sem_op;
//信号量在一次操作中需要改变的数据
//通常是两个数,一个是-1,表示申请一个资源(P);一个是+1,表示释放一个资源(V)
short sem_flg; // 通常为SEM_UNDO,程序结束,信号量为semop调用前的值
};
semctl
控制信号量信息
int semctl(int sem_id, int sem_num, int command, ...);
参数sem_num为集合中信号量的编号,从0开始。如果只有一个信号量则取值为0,表示这是第一个也是唯一的一个信号量。
参数cmd为执行的操作。通常有:
- IPC_RMID(立即删除信号集,唤醒所有被阻塞的进程)
- GETVAL(根据semnun返回信号的值)
- SETVAL(根据semun设定信号的值)
- GETALL(获取所有信号量的值,第二个参数为0,将所有信号的值存入semun.array中)
- SETALL(将所有semun.array的值设定到信号集中,第二个参数为0)等
使用样例
static void P()
{
struct sembuf op;
op.sem_num = 0;
op.sem_op = -1; //-1表示要申请资源
op.sem_flg = 0;
while (semop(semid, &op, 1) < 0)
{
if (errno != EINTR || errno != EAGAIN)
//如果申请的不是自己想要的资源会报这种错误
{
perror("semop()");
exit(1);
}
}
}
static void V()
{
struct sembuf op;
op.sem_num = 0;
op.sem_op = 1; //+1释放资源
op.sem_flg = 0;
while (semop(semid, &op, 1) < 0)
{
if (errno != EINTR || errno != EAGAIN)
{
perror("semop()");
exit(1);
}
}
}
static void func_add()
{
FILE* fp = fopen(FNAME, "r+");
if (fp == NULL)
{
perror("fopen()");
exit(1);
}
char linebuf[LINESIZE];
P();
fgets(linebuf, LINESIZE, fp);
fseek(fp, 0, SEEK_SET);
fprintf(fp, "%d\n", atoi(linebuf) + 1);
fflush(fp);
V();
fclose(fp);
return;
}
int main()
{
pid_t pid;
semid = semget(IPC_PRIVATE, 1, 0600);
//如果是IPC_PRIVATE就不用IPC_CREAT属性
//如果只设一个信号量,则使用上和互斥量类似
if (semid < 0) {}
if (semctl(semid, 0, SETVAL, 1) < 0) {} //将信号量的值设置为1
for (int i = 0; i < PRONUM; i++)
{
pid = fork();
if (pid < 0) {}
if (pid == 0)
{
func_add();
exit(0);
}
}
for (int i = 0; i < PRONUM; i++)
wait(NULL);
semctl(semid, 0, IPC_RMID);
exit(0);
}
共享内存
共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言函数malloc()分配的内存一样。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
注意!共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。所以我们通常需要用其他的机制来同步对共享内存的访问,例如前面说到的信号量。
shmget
创建共享内存
int shmget(key_t key, size_t size, int shmflg);
第一个参数不用说了,其实就是给共享内存命个名。第二个参数,size以字节为单位指定需要共享的内存容量。shmflg是权限标志,如果要想在key标识的共享内存不存在时创建它的话,可以与IPC_CREAT做或运算。共享内存的权限标志与文件的读写权限一样,比如0644表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。
shmat
创建完共享内存后还不能被任何进程访问,shmat()函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。第三个参数,shm_flg是一组标志位,通常为0。
shmdt
该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用
int shmdt(const void *shmaddr);
参数shmaddr是shmat()函数返回的地址指针,调用成功时返回0,失败时返回-1.
shmctl
控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
第二个参数,command是要采取的操作,它可以取下面的三个值 :
- IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
- IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
- IPC_RMID:删除共享内存段
第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构
使用样例
int main()
{
int shmid = shmget(IPC_PRIVATE, MEMSIZE, 0600);
if (shmid < 0) {}
pid_t pid = fork();
if (pid < 0) {}
char *ptr;
if (pid == 0)
{
ptr = shmat(shmid, NULL, 0);
if (ptr == (void *)-1) {}
strcpy(ptr, "HelloWorld");
shmdt(ptr);
exit(0);
}
else
{
wait(NULL);
ptr = shmat(shmid, NULL, 0);
if (ptr == (void *)-1) {}
puts(ptr);
shmdt(ptr);
shmctl(shmid, IPC_RMID, NULL);
exit(0);
}
}