进程间通信

第十四篇 进程间通信
本篇索引:
1、引言
2、无名管道
3、有名管道
4、信号机制
5、系统V IPC通信
6、ftok函数
7、消息队列
8、信号量(或信号灯)semaphore
9、共享内存
10、域套接字
11、进程间通信的总结

1、引言
由于各个进程的虚拟空进程间的地址是不能够互相访问的,所以每个进程对自己的资源的保护能力天生就是很强的,但是这样一个优点同时也导致了缺点,那就是如果两个进程想要共享资源(也就是通信)的话,会非常的困难(因为地址空间式独立的),所以将学习几种进程间通信(IPC)方式:
•管道通信机制
1)无名管道
2)有名管道
•信号(第十篇)
•ipc通信机制
1)消息队列
2)共享内存
3)信号量
•域套接字
管道和IPC通信方式是本篇的内容,而域套接字是网络编程(第十六篇)的内容,我们知道进程的应用空间之间是相互独立的,没有通信的可能,但是所有的进程却共享同一个内核,所以不管是哪种进程间的通信方式,其通信用的管道(缓存),通信机制都是由内核设置,进程间相互通信的消息实际上都是由内核帮忙代为传递的。
2、无名管道
2.1无名管道特点
1)只能用于具有亲缘关系的进程之间的通信。
2)半双工的通信模式具有固定的读端和写段。
3)无名管道其实就是由内核开出的一个缓存空间,共相关的亲缘进程之间共享用,之所以成为无名管道,因为管道没有文件名称。
4)它是一种特殊的文件,支持read和write,因为没有磁盘空间所以不支持lseek函数。
5)创建两个描述符fd[0]和fd[1],fd[0]用于读,fd[1]用于写。
6)当进程创建好一个管道时,本进程中的读端和写端连在了一起的,如下图所示。
这里写图片描述

7)创建管道的进程fork出子进程,子进程就会继承fd[0]和fd[1]这两个文件描述符,那么与 管道相连就有四个文件描述符,父进程的fd[0]、fd[1]和子进程的fd[0]和fd[1], 如果我们希 望利用者一个管道实现父子进程间双向通信的话,这四个描述符必须都是打开,此时管道的 结构下图所示:
这里写图片描述

当父进程利用其自己的fd[1]向子进程这写数据时,父进程往往会抢在子进程之前利用自己的fd[0]将数据抢先读走,子进程利用自己的fd[0]去读时实际上已经没有了数据,所以只用一个管道就想实现父子进程间的双向通信的话,比较困难,因为会出现数据被抢的情况。
8)为了实现双向通信,父进程需要创建两个管道,每一个管道只能实现单向通信,如下图:
这里写图片描述

关闭掉没有用到的无关描述符,防止被无用导致干扰(关闭打叉的描述符),如下图:

这里写图片描述

绿色的路线专门用于子进程想父进程发送信息,黑色的路线专门用于父进程向子进程发送信息,如此一来就实现了两个亲缘进程间的双向通信。
9)无名管道一般只两个用于亲缘进程之间的点对点通信,如果想在多个亲缘进程间利用无名
管道实现网状结构的通信是不太合适的,因为首先描述符的继承关系过于复杂,其次可能会出现数据被别人或自己抢读的情况。
10)创建无名管道的函数,pipe、pipe2函数。
2.2、pipe,pipe2函数
1)、函数原型和所需头文件

#include <unistd.h>
int pipe(int pipefd[2]);
int pipe2(int pipefd[2], int flags);

2)、函数功能
•pipe函数:创建一个用于亲缘进程之间通信的无名管。
•pipe2函数:功能与pipe同,只是多了一个flags参数,可以进行其它的属性设置。
3)、函数参数
• int pipefd[2]:文件描述符数组,只有两个元素,pipefd[0]存放读管道的描述符,pipefd[1] 存放写管道的文件描述符。
•int flags:pipe2函数才有这个参数,用于设置非阻塞和文件描述符执行后关闭标志,flags
通过如下宏相 | 得到:
O_NONBLOCK:将打开的两个文件描述符pipefd[0]、pipefd[1]设置为费阻塞。当然我们可以
用fcntl进行设置。
O_CLOEXEC:设置此宏后,当exec执行了新的程序后,pipefd[0]、pipefd[1]将会被关闭,我
们open函数也有此O_CLOEXEC是一样的。当然我们也可以用fcntl进行设置。
4)、函数返回值:成功返回0,失败则返回-1,并且errno被设置。
5)、注意
1)利用pipe函数获取到的文件描述符pipefd[0]、pipefd[1]都是阻塞的,并且文件描述符的执
行后关闭标志是打开的,也就是说exec新程序后这两个文件面描述符任然有效。如果我
们希望将文件描述符改为非阻塞并且关闭文件描述符执行后关闭标志,我们无法利用pipe
函数直接做到,但是其它的方法有如下两个:
•利用fcntl对文件描述符进行设置。
•不使用pipe函数而是使用pipe2函数,其flag标志可以设置O_NONBLOCK和
O_CLOEXEC标志。
2)如果pipe2函数的flags被设置为0的话,此时的pipe2函数同pipe函数。
3)在没有设置非阻塞的情况下:
•如果管道中无数据,读管道操作会阻塞。
•写管道时管道空间已满,那么写操作也会阻塞。
4)如果设置了非阻塞:
•读管道时及时没有数据,读管道会立即出错返回,errno被设置为EAGAIN。
•写管道时管道时及时空间已满,写操作也不会阻塞,会立即返回。
4) 如果进程写一个读端不存在(被关闭)的管道,那么该进程将收到内核传来的SIGPIPE
信号,这个信号的默认操作是终止进程,命令行会提示Broken pipe错误提示。
5)在写管道时常数PIPE_BUF规定了内核中管道缓存器的大小.
6)、测试用例
•pipe函数

#include <stdio.h>
int main(void){
        int ret = -1;  char buf[200] = {0};
        /* pfds1[0]==3, pfds1[1]==4, pfds2[0]==5, pfds2[1]==6
         * 新程序里面不能直接使用pfds1[0],pfds2[0],这些变空间,因为它们时exec之前进程
 * 的空间,执行了新的程序后那些空间会被新新程序覆盖,我们只能直接使用继承过
 * 来的这些描述符常量*/
        close(3);
        close(6);
        while(1){   
                sleep(1);
                write(4, "hello\n", 6); 
                ret = read(5, buf, sizeof(buf));
                if(ret > 0) printf("in child %s", buf);;
        }   
        return 0;
}

执行gcc new_program.c -o new_prg
pipe.c

void err_fun(const char *file_name, int line, const char *fun_name, int err_no)
{
        fprintf(stderr, "in %s, %d fun %s is fail: %s\n", file_name, line, fun_name, strerror(err_no));
        exit(-1);
}

int main(void)
{
        char buf[300] = {0};
        int pfds1[2] = {0}, pfds2[2] = {0}, ret = -1, flag = -1; 

        /* 创建两个无名管道,创建完成以后
         * pfds1[0]==3, pfds1[1]==4, pfds2[0]==5, pfds2[1]==6 */
        if((pipe(pfds1) < 0) || (pipe(pfds2) < 0)  err_fun(__FILE__, __LINE__, "pipe", errno);

        /* 父子进程双向通信 */
        ret = fork();
        if(0 == ret)
        {
                /* 执行新程序 */
                execl("./new_prg", NULL);
        }
        else if(ret > 0)
        {
                /* 关闭子进程中没有用上的描述符 */
                close(pfds2[0]);
                close(pfds1[1]);
                while(1)
                {
                        sleep(1);
                        ret = read(pfds1[0], buf, sizeof(buf));
                        if(ret > 0) printf("in pareant %s", buf);
                        write(pfds2[1], "world\n", 6);
                }
        }

        return 0;
}

执行如下步骤:
gcc pipe.c
./a.out
运行结果如下:
in pareant, child say:hello
in child, parant say:world
in pareant, child say:hello
in child, parant say:world
in pareant, child say:hello
in child, parant say:world
。。。。。。
结果说明了如下几个问题:
1)说明父子进程双向通信成功,因为父进程收到了子进程发送的hello,子进程收到父进
程发送的world。
2)子进程中运行的新程序依然能够使用管道的读写文件描述符,说明文件描述符的执行新程序关闭标志是开着的。
3)我们看父进程里面和子进程执行的新程序里面,一定有一个的写操作是在读操的前面
的,如果我们都把读操作调到写操作的前面,然后编译运行发现没有任何的数据被打印
出来,那是因为pipe文件描述得到的文件读描述符是阻塞,两边如果没有一个人实现先
写数据而都是在苦苦的等待对方写数据,这就导致两边出现相互阻塞等待的情况,解决 的方法是:
a)将其中一方的写操作调到读操作前面去,我们之前就是这么做的。
b)将描述符改为非阻塞的。
•利用fcntl改为费阻塞
int flag = -1;
flag = fcntl(pfds1, F_GETFL);
flag |= O_NONBLOCK;
fcntl(pfds1, F_SETFL);
•使用pipe2函数
if((pipe(pfds1, O_NONBLOCK) < 0) || (pipe(pfds2, O_NONBLOCK) < 0))
err_fun(FILE, LINE, “pipe”, errno);
4)如果我们还希望在子进程执行完新的程序后关闭继承的所有描述符应该怎么做呢?
a)使用fcntl函数
int flag = -1;
flag = fcntl(pfds1[1], F_GETFD);
flag |= FD_CLOEXEC;
fcntl(pfds1[1], F_SETFD);
b)使用pipe2函数
if((pipe(pfds1, O_CLOEXEC) < 0) || (pipe(pfds2, O_CLOEXEC) < 0))
err_fun(FILE, LINE, “pipe”, errno);
如果既想设置非阻塞又想关闭执行关闭标志,就必须同时指定O_NONBLOCK和O_CLOEXEC,
if((pipe(pfds1, O_NONBLOCK|O_CLOEXEC) < 0) || \
(pipe(pfds2,O_NONBLOCK| O_CLOEXEC) < 0))
err_fun(FILE, LINE, “pipe”, errno);

•pipe2函数:pipe2函数的例子清自己实现。
3、有名管道
3.1、特点
1)无名管道只能用于亲缘进程,但是有名管道却能用于非亲缘进程之间。
2)有名管道创建好后,顾名思义是具有文件名,所以称为有名管道。
3)无名管道其实就是由内核开出的一个缓存空间,共相关的亲缘进程之间共享用,之所以成
为无名管道,因为管道没有文件名称。
4)进程通过文件io来读写有名管道
•单向通信
如果A进程想要发送信息给B进程,A进程只需以只读或带读的方式打开有名管道,B进程则以只写或带写的方式打开同一个有名管道,然后A进程写数据给B进程即可实现通信。
•双向通信
与无名管道一样,想只通过一个管道实现两个进程间的双向通信是不行的,因为也会出现数据被自己抢走的可能,因此同样需要两个管道以实现双向通信,每个管道只负责一个方向的单向通信。
5)先写入管道的先被读出
6)同样不支持lseek函数
7)创建有名管道函数mkfifo函数,该函数只是创建出有名管道,如果我们希望对齐进行io
操作,就必须调用open函数将其打开然后返回需要的文件描述符,而我们的无名管道是使用
pipe函数直接获取稳健描述符的,因为无名管道没有名字所以没办法使用open函数。
3.2、mkfifo函数
3)、函数原型和所需头文件

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

4)、函数功能:被用于创建名叫pathname的有名管道文件。
3)、函数参数
•const char *pathname:用名管道的路径名。
•mode_t mode:同open时的mode参数,一般我们都制定为0664,我们实际创建的有
名管道文件的初始权限等于(mode & ~umask)。
4)、函数返回值:成功返回0,失败则返回-1并且errno被设置。
5)、注意
1)如果打开时未指定O_NONBLOCK的话
a)以只读方式打开将会一直阻塞直到本管道在某地方以带写的方式被打开为止
b)以只写方式打开将会一直阻塞直到本管道在某地方以带读的方式被打开为止
c)如果管道中无数据,读管道操作会阻塞。
d)如果管道中的数据一直没有被读走,且管道空间已满,那么写操作也会阻塞。
2)如果打开时指定了O_NONBLOCK的话
a)以只读打开,不管本管道在某地方有没有以带写的方式打开,都将直接返回,并 且不会报错
b)以只写打开,如果本管道在某地方没有以带读的方式打开,将直接返回,并报错。
如果其它地方有以带读的方式打开的话,将正确返回。
c)读管道时即如果没有数据会立即出错反回,errno被设置为EAGAIN。
d)写管道时,即便管道空间已满,但写操作也不会阻塞。
3)同无名管道一样,如果进程写一个读端不存在(被关闭)的管道,那么该进程将收到
内核传来的SIGPIPE信号,这个信号的默认操作是终止进程,命令行会提示Broken pipe错误提示。
6)、测试用例
我们打算让两个无关进程自由通信,比如A进程从键盘输入数据通过有名管道给B进程,B进程也从键盘输入数据听过有名管道发送给B进程,这里会出现一个问题,我们知道正常情况下度键盘和读管道都是阻塞,那么怎么才能够让他们能不会相互阻塞的工作了呢?听过第十二篇的学习知道有如下方法:
1)将标准输入(键盘)和管道的读都设置为费阻塞的。
2)多进程实现:父进程读键盘并写管道,子进程读管道并打印显示。
3)多线程实现:原理同多进程。
4)多路复用:select或者poll机制均可
5)采用异步通知实现。
以上几种方法我们在第十二篇有详细讲解所以就不在赘述,这里我们采用多进程来实现,
当然我们为了实现双向通信,任然需要创建两个有名管道,
fifo1.c

#define FIFO_NAME1 "./fifo1" //管道1的路径名
#define FIFO_NAME2 "./fifo2" //管道2的路径名

/* 出错处理函数 */
void err_fun(const char *file_name, int line, const char *fun_name, int err_no)
{
        fprintf(stderr, "in %s, %d fun %s is fail: %s\n", file_name, line, fun_name, strerror(err_no));
        exit(-1);
}

/* 创建和打开管道 */
void mk_open_fifo(int *fd, char *fifo_name, mode_t creat_mode, mode_t open_mode)
{    
        int ret = -1; 

        /* 创建名叫file_name的管道文件,如果创建管道文件时发现文件
         * 已经存在,将会停止创建改名字的文件,因为文件存在而引起
         * 出错返回,而因文件存在而引起的错误应该将被忽略 */
        ret = mkfifo(fifo_name, creat_mode);
        if(ret<0 && EEXIST!=errno) err_fun(__FILE__, __LINE__, "mkfifo", errno);

        *fd = open(fifo_name, open_mode); //打开名叫file_name的管道文件
        if(*fd < 0) err_fun(__FILE__, __LINE__, "open", errno);
}

int main(void)
{
        char buf[300] = {0};
        int ret=-1, fd1 =-1, fd2=-1;

        /* 创建两个有名管道 */
        mk_open_fifo(&fd1, FIFO_NAME1, 0664, O_WRONLY);
        mk_open_fifo(&fd2, FIFO_NAME2, 0664, O_RDONLY);

        ret = fork();
        if(0 == ret)    //子进程读键盘写管道1
        {
                while(1)
{
                        bzero(buf, sizeof(buf));
                        ret = read(0, buf, sizeof(buf));
                        if(ret > 0) write(fd1, buf, strlen(buf));
                }
        }

        else if(ret > 0)//父进程读管道2打印显示
        {
                while(1)
                {
                        bzero(buf, sizeof(buf));
                        ret = read(fd2, buf, sizeof(buf));
                        if(ret > 0) write(1, buf, strlen(buf));
                }
        }

        return 0;
}

gcc fifo1.c -o fifo1

fifo2.c

#define FIFO_NAME1 "./fifo1" //管道1的路径名
#define FIFO_NAME2 "./fifo2" //管道2的路径名

/* 出错处理函数 */
void err_fun(const char *file_name, int line, const char *fun_name, int err_no)
{
        fprintf(stderr, "in %s, %d fun %s is fail: %s\n", file_name, line, fun_name, strerror(err_no));
        exit(-1);
}
/* 创建和打开管道 */
void mk_open_fifo(int *fd, char *fifo_name, mode_t creat_mode, mode_t open_mode)
{    
        int ret = -1; 

        /* 创建名叫file_name的管道文件,如果创建管道文件时发现文件
         * 已经存在,将会停止创建改名字的文件,因为文件存在而引起
         * 出错返回,而因文件存在而引起的错误应该将被忽略 */
        ret = mkfifo(fifo_name, creat_mode);
        if(ret<0 && EEXIST!=errno) err_fun(__FILE__, __LINE__, "mkfifo", errno);

        *fd = open(fifo_name, open_mode); //打开名叫file_name的管道文件
        if(*fd < 0) err_fun(__FILE__, __LINE__, "open", errno);
}

int main(void)
{
        char buf[300] = {0};
        int ret=-1, fd1 =-1, fd2=-1;

        /* 创建两个有名管道 */
        mk_open_fifo(&fd1, FIFO_NAME1, 0664, O_RDONLY);
        mk_open_fifo(&fd2, FIFO_NAME2, 0664, O_WRONLY);
        ret = fork();
        if(0 == ret)    //子进阻塞程读管道1并打印读到的数据
        {
                while(1)
{
                        bzero(buf, sizeof(buf));
                        ret = read(fd1, buf, sizeof(buf));
                        if(ret > 0) write(1, buf, strlen(buf));

                }
        }
        else if(ret > 0)//父进程阻塞读键盘,并将读到数据写到管道2中
        {
                while(1)
                {                        
bzero(buf, sizeof(buf));
                        ret = read(0, buf, sizeof(buf));
                        if(ret > 0) write(fd2, buf, strlen(buf));
                }
        }

        return 0;
}


fif02.c中的粉红色部分是与fifo.c不同的部分,恰好与fifo1.c中的该部分相反。
gcc fifo2.c -o fifo2
打开两个控制终端,分别cd到可执行文件fifo1和fifo2所在的目录下,在其中的一个控制终端下执行./fifo1,然后在另一个控制终端下执行./fifo2,这样在第一个控制终端下从键盘敲入”hello”,第二个控制终端会打印出”hello”,如果在第二个控制终端下从键盘输入”world”,第一个控制终端会显示”world”,过程如下图所示:
这里写图片描述

这里使用多进程实现同时阻塞的读键盘和读管道的,使用多进程实现时有个显现需要说明下,那就是当我们在运行fifo1的控制终端下按下ctrl+c,正在运行的fifo1中的父子进程都会被终止,当我们另一个执行fifo2的终端写入数据希望发送时fifo2却突然终止了,但是我们ps查看与当前控制终端相关的进程时发现:
执行ps,结果如下:
。。。。。。
26556 pts/43 00:00:01 fifo2
。。。。。。
发现居然还有一个名叫fifo2的进程正在运行,导致这种现象的原因是因为我们ctrl+c终止fifo1时,fifo1中的父进程终止时关闭掉了管道1中的读端,所以fifo2中的父进程试图去写管道2是被发送了一个SIGPIPE信号而导致终止,父进程终止后是的与控制终端的交互权交还给了终端而造成fifo2结束的假象,但实际上fifo2中的子进程还在运行,只是由于父进程退出变成了孤儿进程,其父进程变成了ID为1 的init进程。
相反如果我们先ctrl+c终止fifo2的话,再在执行fifo2控制终端输入数据时发现fifo1并没有被终止,但是我们在其它终端ps -a结果如下:
。。。。。。
0 R 500 31131 26509 26 80 0 - 459 - pts/42 00:00:28 fifo1
1 Z 500 31148 31131 0 80 0 - 0 ? pts/42 00:00:00 fifo1 < defunct >
。。。。。。
发现父进程(ID小的那个fifo1)正在运行,而子进程(ID大的那个fifo1)变成僵尸进程,也就是说子进程实际上已经结束,只是由于其父进程还未结束所以子进程结束只能称为僵尸进程,导致这样的运行时当fifo2被终止时关闭管道1的读端,当fifo1中的子进程试图写管道1时被发送的SIGPIPE信号所终止而变成了僵尸进程。
如果我们不希望SIGPIPE终止进程的话,我们可以忽略或则屏蔽该信号。
3.3、讨论区
我们如果希望利用有名管道进行复杂的多进程间网状自由通信的话还比较困难,但是就一个管道而言它支持多进程发送而一个进程接收,
这里写图片描述

但是不支持一个进程发送而多个进程同时接收,因为相互间会抢数据,
这里写图片描述

基于fifo上述的特点,我们可利用fifo实现本机内客户进程-服务器进程之间的通信,
这里写图片描述

1)众多客户同时写一个管道,服务器接收,对于fifo来说是可以的。
2)服务器回答是只用一个管道是没有办法实现的,所以针对每个客户都有一个都有一个专属于该客户的FIFO(每当服务器通过众所周知的FIFO知道了某个客户上线了,就为其创建和打开一个该客户独有的专属管道,管道可以用其PID命名)。
6、信号机制
固定通信信息的异步通信机制(属于第十篇内容,这里不再赘述)。
7、系统V IPC通信
前面的无名管道和有名管道都是早期UNIX系统提供的比较原始的一种进程间通信(IPC)方式,后来的系统V(UNIX system V:AT&T于1983年发布)又提供了三种新的IPC通信方式,我们称系统V IPC,它们分别是:
•消息队列
•信号量(也称为信号灯),用于资源保护
•共享内存
这三种新的IPC虽然各有不同,但是它们的实现却也有很多的相似之处,本节我们首先描述它们的相似点,然后再在后面对它们各自进行详细讲解。这三种IPC与前面管道的最大区别就是,对管道我们可以用文件IO函数对其进行操作,但是系统V IPC却不可以,因此系统为这些新的IPC多加了十几个专用的系统调用,专门用于对它们操作。
5.1、IPC标识符
我们知道使得进程间能够通信的方法,是利用所有进程所共享的内核来实现信息的中转,因此我们看到内核为进程间的通信提供了巨大的贡献。前面的管道(不论无名、还是有名管道)是如此,这里的系统V IPC也是如此。
如果我们创建了某种系统V IPC中通信方式,相应地在内核中会给该IPC创建一个IPC结构,并且该结构会被分配一个非负整数的标识符。如果我们希望用某种系统V IPC进行通信,我们就必须通过其被分配的标识符进行发问,以实现信息的收发。
当A进程创建了一个IPC结构并且返回一个标识符,而B进城希望利用A进程创建的IPC结构和A进程通信,只要A和B进程利用相同的标识符访问到相同的IPC结构,那么他们之间就可以通信了。
5.2、关键字(key值)
创建各种系统V IPC的方法是各自调用如下自己的创建函数:
msgget(消息队列)
semget(信号量)
shmget(共享内存)
函数会返回各自相应的标识符,但是这几个函数使用需要用到关键字(key值),它的类型是key_t(通常在头文件

struct ipc_perm
 {
key_t  __key;       /* Key supplied to shmget(2) :关键字*/
uid_t   uid;         /* Effective UID of owner :当前正在使用队列进程的有效用户ID*/
gid_t   gid;         /* Effective GID of owner :当前正在使用队列进程的有效组ID*/
uid_t   cuid;        /* Effective UID of creator :创建者进程的有效用户ID*/
gid_t   cgid;        /* Effective GID of creator :创建者进程的有效组ID*/
unsigned short mode;  /* Permissions:读写权限*/
unsigned short __seq;  /* Sequence number:槽使用顺序号*/
};

备注:什么是槽使用顺序号?
答:ipc_perm结构中还含有一个名为__seq的变量(是一个静态变量),它是一个槽位使用情况的序列号。该变量是内核为每个潜在的IPC对象维护的计数器,每当删除一个IPC对象时,内核就递增相应的槽位号,若溢出则回0。递增槽位使用情况序列号的另一个原因是为了避免短时间内重用system V IPC标识符。这有助于确保过早终止的IPC重启后不会重用标识符。

在创建IPC结构时,除seq以外的所有字段都赋初值,因为seq是一个静态变量,它会保留上一次的修改值。我们可以调用msgctl、semctl或shmctl获取这些初始设置值和修改uid、gid和mode等字段。修改的前提是调用进程要么是超级用户或者是IPC结构的创建者。更改这些字段类似于对文件调用chown和chmod函数。对于任何IPC结构来说不存在执行许可权。另外,消息队列和共享存储使用术语“读”和“写”,而信号量则用术语“读”和“更改”。
5.6、内核对 系统V IPC实现 的限制
内核对System V IPC的多数实现是有限制的,例如消息队列的最大数目、每个信号量集的最大信号量数。
5.7、系统V IPC的缺点
缺点一:IPC结构是在系统范围内起作用,且没有访问计数。
例如,如果创建了一个消息队列,在该队列中放入了几则消息,然后进程终止,但是该消息队列及其内容并不被删除。它们余留在系统中直至:
1)由某个进程调用msgrcv或msgctl读消息或删除消息队列。
2)或者由某个进程执行ipcrm命令删除消息队列。
3)或由正在再起动的系统删除消息队列。
将此与管道pipe相比,当最后一个访问管道的进程终止时,管道就被完全地删除了。对于FIFO而言虽然当最后一个使用FIFO的进程终止时其名字仍保留在文件系统中,直至显式地删除(rm)它,但是留在FIFO中的数据却被全部删除。
缺点二:IPC结构并不按名字被文件系统管理,因此不能用常规的文件系统函数来存取它 们或修改它们的特性。
为此不得不增加了十多个全新的系统调用(msgget、semop、shmat等)。而且不能用ls命令见到它们,不能用rm命令删除它们,不能用chmod命令更改它们的存取权。于是,也不得不增加了全新的命令ipcs和ipcrm。
缺点三:这些IPC不使用文件描述符,所以不能对它们使用多路转接I/O函数:select和 poll。
所以例如就不能多路复用阻塞的同时等候多个消息队列消的消息。
5.8、ipcs和ipcrm命令
前面说过由于System V IPC不被文件系统管理,因此无法使用标准的ls和rm程序(命令)看到和删除它们。但幸运的是系统提供两个特殊的程序(命令):ipcs和ipcrm。ipcs输出有关System V IPC的各种信息,ipcrm则删除一个System V消息队列、信号量集或共享储存区。
1)ipcs的使用选项
-m:显示共享内存
-q:显示消息队列
-s:显示信号量
-a:显示所有,默认就是该选项
2)ipcrm的使用选项
•删除共享内存
-M key:按照键值删除
-m id:按照标识符删除
•删除消息队列
-Q key:按照键值删除
-q id:按照标识符删除
•删除信号量
-S key:按照键值删除
-s id:按照标识符删除
6、ftok函数
1)、函数原型和所需头文件

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

2)、函数功能
ftok函数将文件路径名pathname(该文件必须存在且可访问)和课题ID proj_id的最后8位生成一个key值,该key值适用于msgget、semget和shmget着三个函数。
只要路径名中的文件名和课题ID后8位相同,那么将会产生相同key值,否者不同。生成key值由文件的inode节点号加上课题ID得到。
3)、参数说明
•const char *pathname:文件路径名,一般指定为当前路径 “.”。
• int proj_id:课题ID,虽然是整形,但是只有低8位有效,所以一般我们都指定为字符的ascii的值。
4)、返回值:成功返回key_t型的可以值,失败返回-1,并设置errno。
5)、注意点:无
6)、简单用例:略
7、消息队列
7.1、什么是消息队列
消息队列是创建者在内核中创建的存放消息的链表,这个链表就像是一个队列一样,所以我们又将其形象的称为消息队列,其特点是:
a)消息队列有一个IPC结构的标识符;
b)队列中的每一个消息都有一个消息类型ID;
c)各种类型消息通过链表挂接在一起;
d)按类型进行消息的发送和接受;
队列中存放的每一个消息都有一个消息ID号,这些消息的存取都有ID进行区分,如下图,描述了多个进程之间利用消息队列进行通信的模型。

这里写图片描述

利用消息队列可以实现多进程交叉网状通信,这是前面讲的管道很难做到的,但是消息队列的一个缺点是,发送和接收的数据量受到很大的限制。
7.2、对消息队列的操作步骤、涉及函数
1)获取key值,如果是利用路径名和课题ID获取key值的话,我们需要用到ftok函数
2)利用key值创建消息队列并返回标识符,函数msgget
3)利用队列标识符发送某类型消息,函数msgsnd
4)利用队列标识符接收某类型消息,函数msgrcv
5)利用队列标识符控制消息队列(多用于删除消息队列),函数msgctl
7.3、msgget函数
1)、函数原型和所需头文件

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);

2)、函数功能:利用key值获取创建一个符合msgflg权限要求的消息队列,并返回其标识符。
3)、参数说明
•key_t key:key值,其指定的方式有如下三种:
a)指定为宏IPC_PRIVATE,指定为该宏后,总是创建一个新的消息队列。
b)自己指定一个长整形的key值,但可能会出现key值已经被使用而导致函数出错返回的情况。
c)用ftok函数将指定的路径名和课题ID生成为key值,但也可能会出现key值已经被使用而导
致函数出错返回的情况。
• int msgflg:指定读写权限(例如0664,同open的mode权限设置),除此外还可以 | 如下宏:
a)IPC_CREAT:如果希望创建新的消息队列就需要指定该宏,但是创建时发现key已经和之前
的某个消息队列结合了,那么即便是指定IPC_CREAT,也不会创建出新的消 息队列而只是获取那个已经存在的队列。如果key被指定为IPC_PRIVATE的
话,一定会创建新的消息队列,但是最好还是指定IPC_CREAT。
b)IPC_EXECL:与IPC_CREAT同时指定时,如果key已经和某个消息队列结合了,函数将出 错返回,errno被设置为EEXIST,这一点同O_CREAT和O_EXECL的关系。
4)、返回值:成功返回int型的非负数的标识符,失败返回-1,并设置errno。
5)、注意点:无
7.4、msgsnd和msgrcv函数
1)、函数原型和所需头文件

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

2)、函数功能
msgsnd:在写权限允许的情况下发送消息到消息队列上。
msgrcv:在读权限允许的情况下从消息队列中接收消息。
3)、参数说明
•int msqid:消息队列的标识符。
•void *msgp:一个struct msgbuf类型的指针,struct msgbuf:
“`
struct msgbuf
{
long mtype; /* 消息类型,必须> 0 */
char mtext[msgsz]; /* 消息正文内容 */
};

“`

•size_t msgsz:消息正文大大小。
•long msgtyp:设置接收消息的类型,只有msgrcv函数才有此参数,它有如下设置:
msgtype= =0:取队列中的第一个消息;
msgtype>0:取队列中第一个msgtype类型的消息;
msgtype<0:取消息类型<|msgtype|中最小类型消息的第一个消息;
•int msgflg:
对于msgsnd:有如下设置
0:默认阻塞发送消息,但是如消息队列的IPC结构已经被删除,阻塞发送将立即出错返回, errno被设置为EIDRM(表示IPC结构已经被删除);
IPC_NOWAIT:非阻塞方式发送消息,发送不成功立即返回,errno被设置为EAGAIN;
对于msgrcv:有如下设置
MSG_NOERROR:如果msgrcv函数中指定的msgsz<消息的实际正文长度时,如果该标志
被设置了:消息正文会被截断为msgsz,然后被取走,截断部分会被丢失。
未被设置:msgrcv调用出错返回-1,errno被设置为E2BIG,消息继续留
在队列中(消息接收失败)。
IPC_NOWAIT:非阻塞接收数据,如果没有所需类型数据的话立即返回,errno被设置为ENOMSG。
4)、返回值:
msgsnd调用成功返回0,msgrcv调用成功则返回实际接收并复制到mtext中的消息字节数,如果这两个函数都调用失败则都返回-1,并且errno被设置
5)、注意点
1)一旦消息发送成功,那么用于描述消息队列的struct msqid_ds结构体中如下三个成员 项的值将会被修改,
•msg_lspid:将最后一次向队列发送消息进程的PID设置为当前调用msgsnd函数的
进程PID;
•msg_qnum:队列中消息条数 + 1;
•msg_stime:将最后一次向队列发消息的时间设置为当前调用msgsnd后的时间;
2)同理,一旦消息接收成功,struct msqid_ds中有关接收消息的三个成员项的值将会被
修改,
•msg_lrpid:将最后一次从队列接收消息的进程的PID,设置为当前调用msgrcv函 数进程的PID;
•msg_qnum:队列中消息条数 - 1;
•msg_rtime:将最后一次从队列接收消息的时间设置为当前调用msgrcv后的时间;
3)对于msgsnd来说:
•默认情况是阻塞发送数据
这种情况下如果发送数据不成功会阻塞,但在阻塞的过程中是可以被信号捕获函数中断的,并且不具备自重启功能(可以手动重启)。
•设置非阻塞发送
如果发送不成功会立即出错返回,errno被设置EAGAIN。
4)对于msgrcv函数来说
•默认情况是阻塞接收数据
在这种情况下,如果队列中没有所需类型数据的话,进程会一直阻塞直到,
a)有了指定类型的消息
b)或者此队列已经被删除,则出错返回EIDRM
c)又或者捕捉到一个信号而被信号捕获函数中断,msgrcv函数比具备自动重 启功能,可手动重启。
•设置非阻塞发送
如果接收不成功会立即出错返回ENOMSG。
7.5、msgctl函数
1)、函数原型和所需头文件

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

2)、函数功能
利用buf的设置按照cmd的要求去控制msgid指向的消息队列,当然这三个参数是否要被有cmd的具体设置决定,如果不需要第三个参数的话,就设置为NULL即可,比如利用msgctl删除队列时就不需要第三个参数。struct msqid_ds的成员项如下:

struct msqid_ds {
       struct ipc_perm  msg_perm; /* 消息队列的许可权和所有者,其结构成员向项看5.5小节 */
       time_t  msg_stime;    /* 最后一次向队列发送消息的时间*/
       time_t  msg_rtime;    /* 最后一次从消息队列接收消息的时间 */
       time_t  msg_ctime;    /* 消息队列属性(如所有者、mode等)最后一次被修改的时间 */
       unsigned  long __msg_cbytes; /* 队列中当前所有消息总的字节数 */
       msgqnum_t  msg_qnum;     /* 队列中当前消息的条数*/
       msglen_t msg_qbytes;  /* 队列中允许的最大的总的字节数 */
       pid_t  msg_lspid;     /* 最后一次向队列发送消息的进程PID */
       pid_t  msg_lrpid;     /* 最后一次从队列接受消息的进程PID */
 };

3)、参数说明
•int msqid:指向消息队列的标识符。
•int cmd:控制命令,常用三个设置如下,
IPC_STAT:从内核中复制有关msgid指向的消息队列的属性信息到指针buf指向
的struct msqid_ds结构体中,当然前提是,我们对于消息队列必须拥
有读权限。
IPC_SET: 将设置在buf指向结构体中的值回写到内核中的struct msqid_ds结构中去,实
现对队列属性的修改和控制,显然msg_ctime会被从新设置。
IPC_RMID: 立即删除消息队列,然后唤醒所有向该消息队列阻塞读或阻塞写的进程(出错
返回,并且errno被设置为EIDRM),但如果想要删除队列的前提是,删除进
程只要满足下面几点中的任何一点就可,
a) 删除进程具有优先权(如root用户);
b) 删除进程的有效用户ID = =创建者进程的有效用户ID;
c) 删除进程的有效用户ID= =当前正在队列的进程的有效用户ID;
•struct msqid_ds *buf:该结构体主要用于获取或者修改队列属性信息的,结构成员项已经在前面的 函数功能部分已经描述过了。
5)、返回值:
函数调用成功且cmd被设置为IPC_STAT, IPC_SET, and IPC_RMID中某一个时返回0,否者失败返回-1,且errno被设置。
5)、注意点:对于msgctl函数,最常见的用法就是用来删除一个消息队列。
6)、测试用例
在这个例子中我们实现多个人之间的网状通信,每一个程序有自己的消息ID,每个程序值接受自己消息ID的消息,而发送者根据需求设置被发送消息的类型为接收者的消息ID即可。但是刚开始默认每个人发送的消息都是发给自己的。
msg.c

#define MSG_FILE "./msg_file" //文件类型
#define SIZE 2048 //消息正文的大小
/* 消息包 */
struct msgbuf {
        long mtype;       /* 消息类型, 必须 > 0 */
        char mtext[SIZE]; /* 消息正文 */
};
int msgid;      //存放读列标识符
long my_type;   //存放接收类型类型
long snd_type;  //存放发送消息类型

void err_fun(const char *file_name, int line, const char *fun_name, int err_no) {
        fprintf(stderr, "in %s, %d fun %s is fail: %s\n", file_name, line, fun_name, strerror(err_no));
        exit(-1);
}

/* 创建新的或者获取已有消息队列,mode:创建或获取队列的权限proj_id:课题ID */
void mk_get_msg(mode_t creatmsg_mode, int proj_id)
{
        int fd = -1;
        key_t key = -1;

        /* 创建一个新文件,起文件的路径名用于生成key值,如果文件已经存在
 * 就不必再创建新文件,也不必报文件存在的错误 */
        fd = open(MSG_FILE, O_CREAT, 0664);
        if(fd<0 && EEXIST!=errno) err_fun(__FILE__, __LINE__, "open", errno);

        /* 利用文件路径和课题ID获取key值 */
        key = ftok(MSG_FILE, proj_id);
        if(key < -1) err_fun(__FILE__, __LINE__, "ftok", errno);
/* 如果key没有被用,则创建新的消息队列,否则直接打开已有队列 */
        msgid = msgget(key, IPC_CREAT|creatmsg_mode);
        if(msgid < 0) err_fun(__FILE__, __LINE__, "msgget", errno);
}

/* 信号捕获函数,用于捕获SIGINT和SIGQUIT两个函数 */
void signal_fun(int signo)
{
        int ret = -1;

        /* 修改发送消息的类型 */
        if(SIGINT == signo){
                printf("peer_type:");
                scanf("%ld", &snd_type);
        }
        /* 删除消息队列并 */
        else if(SIGQUIT == signo)
        {
                ret = msgctl(msgid, IPC_RMID, NULL);
                if(ret < 0) err_fun(__FILE__, __LINE__, "msgget", errno);
                remove(MSG_FILE); //删除用于生成key值的文件

                exit(0);  //终止进程
        }
}
int main(int argc, char **argv)
{
        int ret = -1;
        struct msgbuf msgbuf = {0}; //定义消息包

        /* 命令行参数输入本进程接收消息的类型 */
        if(1 == argc){
                printf("exec_file msgtype\n");
                exit(-1);
        }

        /* 将发送消息和接受消息的类型设置为一样,开始时自己发送的消息会被自己接受到 */
        my_type = snd_type = atol(argv[1]);

        /* 创建或获取已有消息队列,0664是创建或获取队列的权限,'a'是获取key用的课题ID */
        mk_get_msg(0664, 'a');

        ret = fork();
        if(0 == ret) {  //子进程负责发送消息
                signal(SIGINT, signal_fun);//捕获SIGINT信号,用于设置发送消息的类型
                signal(SIGQUIT, signal_fun);//捕获SIGQUIT,用于删除消息队列

                while(1){
                        bzero(&msgbuf, sizeof(msgbuf));//清空消息包
                        sprintf(msgbuf.mtext, "%ld", my_type); //将发送者的类型加入消息中
                        ret = read(0, msgbuf.mtext+strlen(msgbuf.mtext), SIZE);//从键盘读消息
                        msgbuf.mtype = snd_type; //设置发送消息的类型           

                        if(ret > 0){
                                ret = msgsnd(msgid, (void *)&msgbuf, SIZE, 0); //阻塞发送消息
                                if(ret < 0) err_fun(__FILE__, __LINE__, "msgget", errno);
                        }
                }
        }
        else if(ret > 0){  //父进程负责接收消息
                //父进程忽略SIGINT信号,防止被SIGINT终止
//,因为SIGINT备用于设置发送消息的类型
                signal(SIGINT, SIG_IGN);
                while(1) {
                        ret = msgrcv(msgid, (void *)&msgbuf, SIZE, my_type, 0); //阻塞接收消息
                        if(ret < 0) err_fun(__FILE__, __LINE__, "msgget", errno);
                        //打印接收到的消息,并提示是谁发送的消息
                        else if(ret > 0) printf("%c say: %s\n", msgbuf.mtext[0], msgbuf.mtext+1);
                }
        }

        return 0;
}



1)gcc msg.c
2)程序运行三次,并输入每个程序接收消息的类型
./aout 1
./aout 2
./aout 3
3)发送信息,发现每个人发送的信息都被自己收到了,如果1向发送信息给3,就需要按下
ctrl+c,将发送信息的类型设置为3,然后1程序输入消息,3程序就接收到了,但是3程序发送的消息还是被自己收到,如果想向1会发消息的话,同理ctrl+c设置发送消息类型为1即可。
基于1和3之间通信的道理,可以实现1和2之间,2和3之间交叉网状的通信。
8、信号量(或信号灯)semaphore
8.1、信号量作用
信号量不同于前面讲的任何一种IPC机构(管道、FIFO以及消息列队)。它只是一个计数器,只是这个计数数值在进程之间能够被相互通信知晓,当多个进程需要共享同一资源时,信号量用于资源的保护,(比如A进程正在写xxx文件,在写完之前应该受到保护,B进程不应该去写xxx文件)。其实现的保护方式有两种:
1)互斥:进程间访问资源的步调是随机的,但是相互间是一定是互斥的
2)同步:进程按照一定的步调实现对资源的访问,但是进程间也是互斥的
这里必须强调说明的是,同步实际上是一种特殊的互斥。说到资源保护,我们在前面的高级IO篇讲过记录锁,那时是通过检查锁的状态来查看资源的可用性,而这里是通过检查计数值来查看资源的可用性,实际地实现虽然有所不同,但是基本的道理都是一致,都是通过检查某一个记号来查看资源是否可用。
8.2、信号量的资源占用和释放
1)信号量大致操作步骤
a)进程试图使用资源前,先检查控制该资源的信号量。
b)如果信号量的值>0,表示进程可以使用该资源。进程一旦使用资源后立即将信号量值
减1,表示它对该资源占用了一次。
c)如果信号量的值==0,表示资源暂时不可用(可能有人正在使用),该进程睡眠,直
至信号量值大于0(表明有人对该资源释放掉了一次占用)。唤醒所有因为等待而休眠的
进程,最先被切换到执行的进程立即回到第一步,实现对资源使用权的占用。
备注:这里必须清楚,为了准确地操作信号量,所以量值的检查和减1应当是原子操作,为此
信号量的操作通常由内核中实现的。
8.3、双值和计数信号量
双值:常用的信号量形式是双态信号量(binary semaphore)。它控制单个资源,其初始值为1,
双值信号量常用于同步和互斥。
计数:一般而言,信号量的初值可以是任意正值,该值说明有多少个共享资源单位可被共享 使用,常用于统计可使用的资源数量。
8.4、信号量的实现步骤
a)获取key值
b)创建新信号量或者获取已有信号量的集合,semget函数
c)初始化信号量集合中每一个信号量的值,semctl函数
d)pv操作
•p操作:获取资源并操作,资源数减1
•v操作:释放资源,资源数加1,并唤醒相应休眠进程或线程semop
e)信号量删除,也是semctl函数
8.5、semget函数
1)、函数原型和所需头文件

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);

2)、函数功能:根据key值创建新的或者获取已有的信号量集合,并返回其标识符。
3)、参数说明
•key_t key:key获取方式同消息队列。
•int nsems:指定信号量集合中信号量个数。
•int semflg:同消息队列。
4)、返回值:调用成功则返回信号量集合的标识符,失败则返回-1,并且errno被设置。
5)、注意
a)创建的都是一个信号量的集合,如果是被用来实现互斥的,那么创建的信号量集合中
只有需要一个信号量就行。如果被用来实现同步,集合可能是有好几个信号量组成的,
b)信号量的使用种类
•双态信号量:互斥和同步的使用的都是双态信号量(信号量初值都设置为1)。
•计数信号量:用于资源统计。
8.6、semctl函数
1)函数原型和所需头文件

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);

2)函数功能
根据cmd的要求实现对semid指向的信号量的控制,此函数是一个变参函数,第四个参数是否需要取决于cmd的要求,如果需要,第四个参数是一个联合体,类型如下:

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) :获取信号量的详细信息*/
};

备注:联合体中val和buf是使用最频繁的两个成员项。
3)参数说明
•int semid:信号量标识符。
•int semnum:信号量集合中某个信号量在集合中的编号。
•int cmd:对信号量的控制命令。常见的设置如下:
•IPC_STAT:将semid指向的信号量的属性信息赋制到第四个参数中,这时第四个参数 可以直接填struct semid_ds *buf指针变量,也可以将其设置为一个联合体,联合体中的
struct semid_ds *buf被赋值存放struct semid_ds结构体变量的地址。
struct semid_ds结构体类型如下:

struct semid_ds
 {
struct ipc_perm sem_perm;  /* Ownership and permissions:信号量权限:请看5.5小节 */
time_t  sem_otime;      /* Last semop time:最后一次PV操作的时间 */
    time_t  sem_ctime;        /* Last change time:最后一次修改信号量属性的时间 */
unsigned short  sem_nsems; /* No. of semaphores in set:在信号量集合中信号量的编号 */
};

•IPC_SET:将第四个参数设置的信号量的属性回写到内核中,已达到修改内核中信号量
属性。第四个参数的设置方式同上,任然需要用到struct semid_ds结构体。
•IPC_RMID:立即删除信号量,立即唤醒因为P操作等待资源而休眠的进程,被唤醒的
进程错误返回,errno被设置为EIDRM,这样求进程的有效有效用户ID等于信号量创建
者或者使用者进程的有效用户ID。
4)返回值:对于以上列举的几个参数,如果函数调用成功,失败则返回-1,errno被设置。
5)注意:semctl用的最多的就是,
a)对集合中的各个信号量的赋初值
b)删除信号量。
8.7、semop函数
1)函数原型和所需头文件

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);

2)函数功能:根实现前面所说的pv操作。
4)参数说明
•int semid:信号量标识符。
•传递nsops个元素的的struct sembuf的结构体数组,每个元素存放信号量集合中需要被
pv操作的信号量pv操作的设置。truct sembuf结构体类型如下:

struct sembuf
{
    unsigned short sem_num;  /* semaphore number:信号量集合中信号量的编号 */
short          sem_op;  /* semaphore operation:指定pv操作 */
    short          sem_flg;  /* operation flags :指定对pv操作的要求 */
};

结构体成员说明:
unsigned short sem_num:需要被pv操作的信号量在集合中的编号。
short sem_op:指定p操作或v操作
a)负整数:p操作,一般设置为-1
b) 正整数:v操作,一般设置为+1
c) 0:这表示希望等待到该信号量值变成0。
short sem_flg:常见设置如下,他们之间可以相 | :
a)0:pv采用默认操作,默认情况下p操作会阻塞(v操作不会阻塞),进程突然终止时,
被-1之后的信号量值(被占用的资源)不会被还原,这可能会导致进程在终止时永远地占用
资源无法被释放。
b)IPC_NOWAIT:指定此标志后。当前信号量的值不满足p操作,也是就是说资源不可被获取时,p操作立即出错返回,errno被设置为EAGAIN,否者进程会因p操作阻塞而休眠。
c)SEM_UNDO:指定此标志后,当进程突然终止时,不管有没有认为的v开进
程占用的资源,内核会主动的检查,会把进程占用的资源释放,将信号量的值还
原为该终止进程(p操作)占用资源前的设置。Undo的意思就是解除终止进程对
资源的占用。
•unsigned nsops:指定传递的struct sembuf数组的元素个数,一般都为1(数组只有一个
元素)。
5)返回值:如果函数调用成功0,失败则返回-1,errno被设置。
6)注意
a)pv操作时,一般情况下都指定只有一个元素的特殊数组,也就是说只对信号量集合中
的某一个信号量做pv操作。
b)p操作时,sem_op一般都指定为-1,v操作时,sem_op一般都指定为1。
c)虽然一次可以对很多歌信号做pv操作,但一般情况下一次只对集合中一个信号量做
pv操作。
d)p操作时,可能会遇到如下情况:
•如果信号量的初始值被设置==0,函数立即返回。
•如果设置的信号量的初始值不为0,此情况下:
-如果sem_flg指定了IPC_NOWAIT:p操作不成功不会阻塞,会立即出错返回EAGAIN。
-如果未指定IPC_NOWAIT,而且资源无法获取(目前信号量值不允许p操作),
则p操作会阻塞,p操作的进程因此而休眠,进程休眠直至下列事件之一发生:
i.资源可以被获取。因等待资源而休眠的进程被唤醒。
ii.从系统中删除了此信号量。在此情况下,函数出错返回ERMID。
iii.进程捕捉到一个信号,并从信号处理程序返回。在此情况下,函数立即
出错返回EINTR 。
f)semop具有原子性。
g)exit时的被本进程pv过的信号量的值应该调整会pv前的值,方法有两种。
•调用如果用带SETVAL或SETALL命令的semctl设置一信号量的值,进行调整。
•通过设置SEM_UNDO标志,等到进程终止后由内核做值的调整。
8.7、信号量的使用举例
我们这里只对双态信号量进行举例。
1)利用信号量实现互斥
我们前面说过两个进程同时向文件里面写”hello world\n”,会产生如下情况:

hello world
hello world
hello hello
......
hello world

我们之前解决的办法是利用记录锁将write(“hello ”); write(“world\n”);变为原子操作,从而达到互斥的效果,也就是说当第一个进程没有完整的将helloworld\n一次性写入文件的话,第二个进程就没有办法写数据,这就是互斥。
这里的例子中我们采用系统V信号量,来取代记录锁以实现进程间的互斥。
sem.c:实现信号量而封装的子函数

/* 头文件自己添加 */
/* 联合体 */
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 */
};

/* 出错处理函数 */
void err_fun(const char *file_name, int line, const char *fun_name, int err_no)
{
        fprintf(stderr, "in %s, %d fun %s is fail: %s\n", file_name, line, fun_name, strerror(err_no));
        exit(-1);
}
/* 创建新信号量或获取已有信号量,获取key存到方法同消息队列
 * semid:获取信号量的标识符
 * sem_file:用于获取key值的文件路径名
 * nsems:指定要在信号量集合中创建出多少个信号量
 * proj_id:获取key用的课题ID
 * creatsem_mode:指定创建或获取信号量时的权限 */
void get_sem(int *semid, const char *sem_file, int nsems, int proj_id, mode_t creatsem_mode)
{
        int fd = -1;
        key_t key = -1;

        fd = open(sem_file, O_CREAT|O_RDWR, 0664);
        if(fd < 0) err_fun(__FILE__, __LINE__, "open", errno);

        key = ftok(sem_file, proj_id);
        if(key < -1) err_fun(__FILE__, __LINE__, "ftok", errno);

        *semid = semget(key, nsems, IPC_CREAT|creatsem_mode);
        if(*semid < 0) err_fun(__FILE__, __LINE__, "semget", errno);
}

/* 给集合中的每个信号量给一个初始值
 * semid:信号量标识符
 * semnum:信号量编号
 * value:初始值 */
void init_sem(int semid, int semnum, int value)
{
        int ret = -1;
        union semun semun;

        /* semctl的第四个参数可以直接填写value,也可直
         * 接写共同体,结果是一样的,只得都是相同的值 */
        semun.val = value;
        ret = semctl(semid, semnum, SETVAL, semun);
        if(ret < 0) err_fun(__FILE__, __LINE__, "semctl", errno);
}

/* 删除信号量 */
void del_sem(int semid, int nsems)
{
        int ret = -1,  i = 0;

        /* 将集合中的每一个信号另都删除 */
        for(i=0; i<nsems; i++){
                ret = semctl(semid, i, IPC_RMID, NULL);
                if(ret < 0) err_fun(__FILE__, __LINE__, "semctl", errno);
        }
}
/* v操作 */
void sem_v(int semid, int semnum, int vn)
{
        int ret = -1;
        struct sembuf sops = {0};

        sops.sem_num    = semnum; //信号编号
        sops.sem_op     = vn; //v值,一半传过来的是+1
        sops.sem_flg    = SEM_UNDO;//设置undo标志

        ret =semop(semid, &sops, 1);
        if(ret < 0) err_fun(__FILE__, __LINE__, "semop", errno);
}







sem.c

#ifndef H_SEM_H
#define H_SEM_H
extern void err_fun(const char *file_name, int line, const char *fun_name, int err_no);
extern void get_sem(int *semid, const char *sem_file, int nsems, int proj_id, mode_t creatsem_mode);
extern void init_sem(int semid, int semnum, int value);
extern void del_sem(int semid, int nsems);
extern void sem_p(int semid, int semnum, int pn);
extern void sem_v(int semid, int semnum, int vn);
#endif

write_hello_woeld.c

#include "sem.h"

#define SEM_FILE "./sem_file"
#define FILE_NAM "./file"
#define NSEMS 1

static int semid; //信号来年个标识符

void signal_fun(int signo) //信号捕获函数
{
        del_sem(semid, NSEMS); //删除信号量
        remove(SEM_FILE); //删除用于创建信号量用的文件

        exit(-1);
}

int main(void)
{
        int i = 0int ret = -1, fd = -1; 
        /* 打开一个文件,乡里面写入hello world\n */
        fd = open(FILE_NAM, O_RDWR|O_CREAT|O_TRUNC, 0664);
        if(fd < 0) err_fun(__FILE__, __LINE__, "open", errno);

        /* 创建新的信号量或者获取已有信号量,NSEMS:集合中信号量的个数 */
        get_sem(&semid, SEM_FILE, NSEMS, 'a', 0664);

        /* 初始化信号量,用于互斥,集合中信号量就只
         * 有一个,编号为0,将其值初始化为1(双态信号量) */
        for(i=0; i<NSEMS; i++) init_sem(semid, 0, 1);

ret = fork();
        if(0 == ret) //子进程
        {
                while(1)
                {
                        sem_p(semid, 0, 1); //p操作
                        write(fd, "hello ", 6);
                        write(fd, "world\n", 6);
                        sem_v(semid, 0, 1); //v操作
                }
        }
        else if(ret > 0) //父进程
        {
                signal(SIGINT, signal_fun); //捕获一个SIGINT,删除信号量个用
                while(1)
                {
                        sem_p(semid, 0, 1); //p操作
                        write(fd, "hello ", 6);
                        write(fd, "world\n", 6);
                        sem_v(semid, 0, 1); //v操作
                }
        }

        return 0;
}



gcc write_hello_world.c sem.c
./a.out
vi file 再也不会出现之前所说的情况了。
2)利用信号量实现同步
在本例中,我们有四个进程,我们分别让这几个进程按照要求的步调打印出
1111111
2222222
3333333
4444444
要完成这样同步的要求,还是需要利用双态信号量来做,但是集合中必须需要四个信号量。
代码如下:

sem_sync.c

#include "sem.h"

#define SEM_FILE "./sem_file"
#define NSEMS 4

static int semid;

void signal_fun(int signo)
{
        del_sem(semid, NSEMS);
        remove(SEM_FILE);

        exit(-1);
}

int main(void)
{
        int i = 0, ret = -1; 

        /* 创建新的信号量或者获取已有信号量 */
        get_sem(&semid, SEM_FILE, NSEMS, 'a', 0664);

        /* 对集合中的每个信号量赋初始值,都赋值为双态信号量
         * 第0个信号量赋值为1,其余的全部赋初始值为0 */
        for(i=0; i<NSEMS; i++)
        {
                if(0 == i) ret = init_sem(semid, i, 1);
                else init_sem(semid, i, 0);
        }

        ret = fork();
        if(0 == ret)
        {
                ret = fork();
                if(0 == ret) //进程1
                {
                        while(1)
                        {
                                sem_p(semid, 0, 1); //p信号量0
                                sleep(1);
                                printf("11111111111\n");
                                sem_v(semid, 1, 1); //v信号来年个1
                        }
                }
                else if(ret > 0) //进程2
                {
                        while(1)
                        {
                                sem_p(semid, 1, 1); //p信号量1
                                sleep(1);
                                printf("22222222222\n");
                                sem_v(semid, 2, 1); //v信号来年个2
                        }
                }
        }
        else if(ret > 0)
        {
                ret = fork();
                if(0 == ret) //进程3
                {
                        signal(SIGINT, signal_fun);
                        while(1)
                        {
                                sem_p(semid, 2, 1); //p信号量2
                                sleep(1);
                                printf("33333333333\n");
                                sem_v(semid, 3, 1); //v信号量3
                        }
                }
                else if(ret > 0) //进程4
                {
                        while(1)
                        {
                                sem_p(semid, 3, 1); //p信号量3
                                sleep(1);
                                printf("44444444444\n");
                                sem_v(semid, 0, 1); //v信号量0
                        }
                }
        }

        return 0;
}


8.7、信号量与记录锁对比
记录锁与信号量锁相比,虽然记录锁稍慢于信号量锁,但如果只需锁一个资源(例如共享存储段)并且不需要使用系统V信号量的所有花哨的功能,则宁可使用记录锁。理由是:
a)使用简易
b)进程终止时,会处理任一遗留下的锁。
9、共享内存
9.1、什么是共享内存
1)回忆mmap,内存映射
我们在第十二篇高级IO中,我们讲过mmap函数,目的就是将磁盘上的普通文件直接映射到虚拟内存空间,以便实现对文件的直接快速地进行读写,实际上利用mmap函数我们也能够进行进程间的通信,如果AB两个进程同时映射了同一个文件,那么AB两个进程完全可以利用共同映射的的同一个磁盘普通文件进行通信,如下图所示:

这里写图片描述

为什么不用mmap这种方式进行进程间同信呢,虽然原理上是可行的,原因就是他们共享的是外存磁盘,而对磁盘的访问速度是很慢的,加入我们要是能够将外存改为内存的话,那么我们的访问速度不是家加快了吗。
所以共享内存就是本小节所要讲的内容,其实现的原理与共享外存(mmap)是类似的,但是共享内存的实现函数就不再是mmap了,而是另外的一些函数。
2)共享内存原理
这里写图片描述

从原理图的结构来看,共享内存和mmap的共享外存差不多,但是唯一的区别就是大家共享是内存,所以进程间通信访问共享内存的速度很快。
3)共享内存的优点和缺点
优点:
(1)效率高,因为共享内存这种通信方式避免了大量的数据赋制的过程,而是通过映射地
址直接实现数据的访存,所以在所有的IPC中是效率最高的一种。
(2)特别适用于进程间对于大量数据文件的共享。
缺点
(1)涉及多个进程同时对同一片空间的访存,例如A进程再写时,其它进程肯定是不能读
的,B进程再读时可以允许其它进程读写或不允许读写共享内存,这就需要加入资源保护机制,如记录锁和前面刚讲的信号量都可以实现。
(2)当多进程间想要利用共享内存实现交叉王庄通信时,必须使用记录锁或者信号量以实现资源的同步或者互斥,以便保护资源。
9.2、共享内存实现步骤
a)创建新的或获取已有共享内存,shmget函数
如果是创建新的共享内存,会从物理内存中专门的劈出一片物理内存出来,以便共享。
b)将共享的物理内存各自映射到自己的进程应用空间地址,shmat函数
c)取消映射,shmdt函数
d)删除从物理内存中开辟出来共享内存,shmctl函数
9.3、shmget函数
1)、函数原型和所需头文件

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

2)、函数功能:根据key值创建新的或者获取已有的共享内存,并返回其标识符。
3)、参数说明
•key_t key:key获取方式同消息队列和信号量。
•size_t size:指定共享内存的大小,我们一般要求size是虚拟页大小的整数倍,一般来
说虚拟页大小是4k,我们可以通过getpagesize函数获取虚拟页大小值。
•int semflg:同消息队列和信号量。
4)、返回值:调用成功则返回共享内存的标识符,失败则返回-1,并且errno被设置。
9.4、shmat和shmdt函数
1)、函数原型和所需头文件

#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);

2)、函数功能
•shmat:将shmid指向的物理内存上的共享内存空间映射到自己的进程空间(虚拟内存
空间)的应用空间,并返回影射后的其实地址。
•ahmdt:将自己进程空间(虚拟内存空间)的应用空间与共享内存空间的映射取消。这 个函数可以不用显示的调用,进程exit时会自动取消映射。
3)、参数说明
•shmat函数
-int shmid:标识符,指向从物理内存上开辟出来的共享内存。
-const void *shmaddr:指定映射地址的起始地址,当指定为
NULL:表示映射起始地址有内核决定,这时最常见的情况。
不为NULL:如果shmflg指定了SHM_RND,将shmaddr以下的某个虚拟页整数
倍地址取为映射起始地址,否则我们就必须将shmaddr指定为虚拟页的整数倍。
-int semflg:指定映射条件。常由如下相|得到:
0:进程对共享内存,可读可写,但是前提是shget时允许读写才行。
SHM_RDONLY:指定以只读方式映射共享内存,进程对共享内存只有读权限。
但是前提是shget时允许读才行。
只读映射方式: 没有这种方式。
SHM_RND: 请看shmat函数的const void *shmaddr说明。
SHM_REMAP: 当shmat时,且shmaddr非NULL,如果发现shmaddr已经被之
前的某个共享内存正映射着,那么本次shmat时会出错返回EINVAL。如果我们
不希望遇到这个情况时就出错返回,那么我们需要在shmflg中指定SHM_REMAP
标志,shmaddr只对最新的一次映射有效。
•shmdt函数
-const void *shmaddr:shmat时得返回到的映射起始地址。
4)、返回值
•shmat函数:调用成功则返回映射地址,失败则返回(void *)-1,并且errno被设置。
•shmdt函数:调用成功返回0,失败返回-1,且errno被设置。
5)、注意
a)一旦shmat函数调用成功,shmid_ds结构体(这个结构体专门用于描述共享内存的当 前状态的,man shmctl可以查看到该结构体)中的如下成员将会被更新。
•shm_atim:对共享内存最近映射的时间被设置为当前时间;
•shm_lpid:最新映射或取消映射的进程PID,设置为当前进程PID;
•shm_nattch:对共享内存的映射数量+1;
b)一旦shmdt函数调用成功,shmid_ds结构体中的相关成员也会被更新。
shm_dtime:最后一次取消映射的时间设置为当前时间;
shm_lpid:最新映射或取消映射的进程PID,设置为当前进程PID;
shm_nattch:对共享内存的映射数量+1;
9.5、shmctl函数
1)、函数原型和所需头文件

#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

2)、函数功能:根据cmd的要求利用buf实现对共享内存属性信息的获取或者是控制。
3)、参数说明
•int shmid:标识符,指向从物理内存上劈出来的共享内存。
•int cmd:常用控制命令如下:
IPC_STAT:从内核里面获取共享内存的属性信息存到buf所指向的struct shmid_ds 结构体空间中,以共查看。struct shmid_ds结构如下:

struct shmid_ds 
{
struct ipc_perm shm_perm;    /* Ownership and permissions:权限 */
size_t shm_segsz;   /* Size of segment (bytes):共享内存大小 */
time_t shm_atime;   /* Last attach time:最后一次映射的时间 */
time_t shm_dtime;   /* Last detach time:最后一次取消映射的时间 */
time_t shm_ctime;   /* Last change time:最后一次修改属性状态的时间 */
pid_t shm_cpid;    /* PID of creator:创建者进程的PID */
pid_t shm_lpid;    /* PID of last shmat(2)/shmdt(2) :当前正在使用者的PID*/
shmatt_t shm_nattch;  /* No. of current attaches:映射数量 */ 
...
};

IPC_SET:将我们在buf中设置的新的共享内存的设置回写到内核中以修改共享内存
的的属性。
IPC_RMID:删除共享内存,只有当所有的映射取消后,删除共享内存才是合理的(也
就是当shm_nattch变为0以后)。如果想要删除共享内存必须具备如下几个条件:
(1)进程具有超级优先权(root权限);
(2)删除进程的有效用户ID等于共享内存的创建者有效用户ID
(3)或者删除进程的有效用户ID等于共享内存使用者的有效用户ID。
•int semflg:同消息队列和信号量。
4)、返回值
对于上面讲的常用的集中cmd命令来说,调用成功则返回共享内存的0,失败则返回-1,并且errno被设置。
5)、注意
1)shmctl函数经常用于删除共享内存;
2)删除共享的内存的前提是,所有的映射都被取消;
3)删除共享内存必须满足的条件需要注意;
9.6、共享内存使用举例
本例中进程A创建一个共享内存并映射到本进程应用空间,进程B获取共享内存也映射到本进程的应用空间,然后A进程发送信息,B进程获取数据并打印出来,这里两个进程的读写之间用该使用记录锁或者信号量实现互斥,但是在例子中并没有使用记录锁或信号量,请同学们自己添加。
shm1.c

#define SHM_FILE "./shm_file"
#define SHM_SIZE 1024

int shmid; //标记共享内存的标识符
void *addr; //存放映射厚的地址

void err_fun(const char *file_name, int line, const char *fun_name, int err_no)
 {
        fprintf(stderr, "in %s, %d fun %s is fail: %s\n", \
file_name, line, fun_name, strerror(err_no));
        exit(-1);
}

void mk_get_shm(mode_t creatshm_mode, int proj_id)
 {
        int fd = -1; 
        key_t key = -1; 
        FILE *fp = NULL;

        /* 创建用于获取key值的文件,可不用指定读写权限 */
        fd = open(SHM_FILE, O_CREAT|O_RDWR, 0664);
        if(fd<0 && EEXIST!=errno) err_fun(__FILE__, __LINE__, "open", errno);

        /* 获取key值 */
        key = ftok(SHM_FILE, proj_id);
        if(key < -1) err_fun(__FILE__, __LINE__, "ftok", errno);

        /* 根据key值创建新的或者获取已有共享内存 */
        shmid = shmget(key, SHM_SIZE, IPC_CREAT|creatshm_mode);
        if(shmid<0 && EINVAL!=errno) err_fun(__FILE__, __LINE__, "shmget", errno);

}

void signal_fun(int signo) 
{
        int ret = -1;

        /* 取消映射 */
        ret = shmdt(addr);
        if(ret < 0) err_fun(__FILE__, __LINE__, "shmdt", errno);

        /* 删除共享内存 */
        ret = shmctl(shmid, IPC_RMID, NULL);
        if(ret<0 && EINVAL!=errno) err_fun(__FILE__, __LINE__, "shmctl", errno);
        ret = shmctl(shmid, IPC_RMID, NULL);
        if(ret<0 && EINVAL!=errno) err_fun(__FILE__, __LINE__, "shmctl", errno);

        remove(SHM_FILE); //删除用于获取key值的文件

        exit(-1); //进程终止
}

int main(void)
{
        int ret = -1;

        /* 捕获SIGINT信号,用于删除共享内存和进程退出 */
        signal(SIGINT, signal_fun);

        /* 创建新的或者获取已有共享内存 */
        mk_get_shm(0664, 'a');

        /* 映射共享内存到本进程空间的应用空间 */
        addr = shmat(shmid, NULL, 0);
        if((void *)-1 == addr) err_fun(__FILE__, __LINE__, "shmat", errno);

        while(1)
        {
                //此位置可加记录锁或者某信号量p操作
                ret = read(0, addr, SHM_SIZE); //从键盘直接读数据到共享内存中
                if(ret < 0) err_fun(__FILE__, __LINE__, "read", errno);
                //记录锁解锁或者对刚才p操作的信号量v操作
        }

        return 0;
}


shm2.c

int main(void)
{
        /* 其余部分完全同shm1.c */

        while(1)
        {
                //此位置可加记录锁或者某信号量p操作
    if(strlen((char *)addr) != 0) printf("%s", (char *)(addr));
bzero(addr, SHM_SIZE);
                //记录锁解锁或者对刚才p操作的信号量v操作
        }

        return 0;
}

10、域套接字
进程间通信还有一种方式,那就是域套接字,这个我们留到网络编程讲。
11、进程间通信的总结
11.1、对比效率和使用频度对比
1)效率
共享内存(shm) > 域套接字(socket)> 管道(pipe or fifo) > 消息队列(message)
2)易用性
消息队列(message) > 域套接字(socket) > (pipe or fifo)管道 > (shm)共享内存
对比结果:最被常用:socket
3)信号:信息量受限的一种异步通信方式
11.2、各种进程间通信方式的异同
1)管道
•无名管道( pipe )
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
•有名管道 (named pipe)
有名管道也是半双工的通信方式,但是它允许非亲缘关系进程间的通信。
管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面
大小),管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数
据的格式,比如多少字节算作一个消息(或命令、或记录)等等;
2)信号量( semophore )
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
3)消息队列( message queue )
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
4)信号 ( sinal )
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
5)共享内存( shared memory)
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
6)域套接字( socket )
套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值