信息安全系统设计基础学习项目_进程间通信

项目名称:进程间通信机制

20135313吴子怡  20135322郑伟

研究时长:两周

内容摘要:管道、FIFO、消息队列、共享内存、信号量、对比机制优缺点

chapter 1   管道与FIFO

part 1 预习

在进程之间进行通信的最简单方法是通过文件,其中一个进程写文件,而另一个进程读文件。
管道是Linux中最常见的IPC机制,它实际上是在进程间开辟一个固定大小的缓冲区,需要发布信息的进程运行写操作,需要接收信息的进程运行读操作。管道是单向的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。
发送进程和接受进程是通过管道进行通信的,因此又称为管道通信。
管道可以和我们生活中具象的管道联系起来理解,就是数据沿着管道从左边流到了右边。

part 2管道的实现机制

管道就是指用于连接一个读进程和一个写进程,以实现他们之间通信的共享文件,又称为pipe文件。

Linux中实现了两种管道,一种是无名管道,一种是命名管道。无名管道没有磁盘节点,仅仅是一个内存对象存在,用完后就销毁了。因为没有文件名和路径,也没有此案节点,因此无名管道没有显式的打开过程,它是在创建的时候自动打开并且声称内存节点inode、目录项对象dentry和两个文件结构对象(读操作、写操作),其内存对象和普通文件一致。无名管道只能由父子进程之间、兄弟进程之间或者其他与偶沁园关系并且都继承了祖先进程的管道文件对象的两个进程间通信使用,命名管道是由文件名和磁盘i的,因此可以由任意两个或多个进程间通信使用,它的使用和普通文件类似,都遵循打开、读、写、关闭这样的过程。

1.无名管道

(1)工作方式

管道以先进先出方式保存一定数量的数据。使用管道的时候一个进程从管道的一端写, 另一个进程从管道的另一端读。在主进程中利用fork()函数创建一个子进程, 然后使用read()和write()函数来进行读写操作。

使用无名管道进行进程间通信的步骤概述如下:

①创建所需的管道
②生成(多个)子进程
③关闭/复制文件描述符,使之与相应地管道末端相联系;
④关闭不需要的管道末端;
⑤进行通信活动
⑥关闭所有剩余的打开文件描述符
⑦等待子进程结束

(2)无名管道的创建:pipe函数

#include <unistd.h>
int pipe(int fd[2])

函数的参数中有两个文件描述符:fd[0]用于管道的read端,fd[1]用于write端。创建成功返回0,否则返回-1。

(3)写管道write函数

ret = write(fd[1],buf,n)

若管道已经满了,则被阻塞,知道管道另一端read将已进入管道的数据取走为止。

(4)读管道read函数

ret = write(fd[0],buf,n)

若管道为空,且写端文件描述字未关闭,则被阻塞。若管道已经关闭则返回0。

管道不为空分两种情况:设管道中实际有m个字节,如n>=m,则读m个;如果n<m则读取n个。实际读取的数目作为read的返回值。

(5)关闭管道close函数

关闭写端则导致读端read调用返回0;关闭读端,则导致写端write调用返回-1,error被设为EPIPE,在写端write函数退出前,进程还会收到SIGPIPE信号(默认处理是种植进程,该信号可以被捕捉)。

(6)文件描述符的复制dup2

int dup2(int fd1,int fd2);

复制文件描述符fd1到fd2。fd2可以是空闲的文件描述符,如果fd2是已打开文件,则关闭fd2;如果fd1不是有效的描述符,则不关闭fd2,调用失败。

(7)注意事项

①管道式半双工方式,数据只能单向传输,若要两个进程之间相互通信,则需要建立两个管道。
②pipe()调用必须在调用fork()以前进行,否则子进程将无法继承文件描述符。
③使用无名管道互相连接的任意进程必须位于一个相关的进程家族里。因为管道必须受到内核的限制,所以如果进程没有在管道创建者的家族里面,则该进程将无法访问管道。

这里我们用几个案例来体会一下:

<<案例一:从管道中读取数据

结论:写端不存在时,此时则认为已经读到了数据的末尾,读函数返回的读出字节数为0。

但是当写端存在时,如果请求的字节数目大于PIPE_BUF(ubuntu操作系统为65536),则返回管道中现有的数据字节数,如果请求的字节数目不大于PIPE_BUF,则放回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。

<<案例二:父进程向管道中写数据,子进程从管道中读取数据

结论:

<1>当写端存在时,管道中没有数据时,读取管道时将阻塞

<2>当读端请求读取的数据大于管道中的数据时,此时读取管道中实际大小的数据
<3>当读端请求读取的数据小于管道中的数据时,此时放回请求读取的大小数据
 

2.命名管道(FIFO:先进先出)

(1)工作方式

无名管道只能用于具有亲缘关系的进程间通信。而命名管道提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。因此,通过FIFO不想管的进程也能交换数据。FIFO管道的打开时需要用户打开和关闭,因为它是一直存在的。Linux必须处理读进程先于写进程打开管道、读进程在写进程写入数据之前读入这两种情况。

(2)FIFO文件的创建

有两种常见的方法:在shell提示符下使用mknod命令或者在程序中使用mknod()系统调用。如:

shell命令行方式:

$ mknod filename p
$ mkfifo a = rw filename

这两个命令都可以创建FIFO文件filename。mkfifo提供了直接改变文件读写权限的功能。mknod创建的文件通过chmod可以改变权限。其中,参数p是所建立的节点,即特殊文件的类型为命名管道。

(3)函数调用

mknod()是一般的设备文件创建函数:

# include <sys/type.h>
# include <sys/stat.h>
int mknod(const char *filename,mode_t mode,dev_t dev)

其中,filename是被创建文件的名称。mode表示将在该文件上设置的权限位和被创建的文件类型;dev是创建设备文件时使用的值。

mkfifo()函数专门用于创建FIFO。

# include <sys/type.h>
# include <sys/stat.h>
int mkfifo(const char *filename,mode_t mode)

此两个函数均调用成功后返回0,否则返回-1。

FIFO可以被许多进程同时访问,如果多个进程在写一个管道时,在管道可容纳的范围内,系统将保证各进程所写的数据是分开的。因此在多个进程的读写应用中FIFO非常有用。

(4)管道必须有人读和有人写。如果某进程企图往管道中写数据,而没有进程去读该管道,则内核将向该进程发送SIGPIPE信号。这在管道操作中涉及两个以上进程时非常必要。

3.有名管道的打开方式:有名管道比无名管道多了一个打开操作:open

如果当前打开操作时为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。
如果当前打开操作时为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENIO错误(当期打开操作没有设置阻塞标志)。
 
4.有名管道的读写规则
(1)从FIFO中读取数据:如果一个进程为了从FIFO中读取数据而以阻塞的方式打开FIFO, 则称内核为该进程的读操作设置了阻塞标志
①如果有进程为写而打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。对于没有设置阻塞标志读操作来说返回-1,当前errno值为EAGAIN,提醒以后再试。
②对于设置阻塞标志的读操作说,造成阻塞的原因有两种:当前FIFO内有数据,但有其他进程正在读这些数据;另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论写入数据量的大小,也不论读操作请求多少数据量。
③如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞
④如果写端关闭,管道中有数据读取管道中的数据,如果管道中没有数据读端将不会继续阻塞,此时返回0。
注意:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。
(2)向FIFO中写入数据:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作设置了阻塞标志。
a.对于设置了阻塞标志的写操作:
①当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳写入的字节数时,才开始进行一次性写操作。
②当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。
b.对于没有设置阻塞标志的写操作:
①当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。
②当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写。
注意:只有读端存在,写端才有意义。如果读端不在,写端向FIFO写数据,内核将向对应的进程发送SIGPIPE信号(默认终止进程);
6.管道的不足

管道提供了一种从进程向另一种进程传输数据的有效方法。但是也有缺陷:

①因为读数据的同时也向另一种进程传输数据,因此管道不能用来广播数据。
②若管道有多个读进程,则写进程不能发送数据到指定的读进程,同样,有多个写进程时也无法判别式其中的哪一个。

7*.附加案例 

Linux下进程之间通信可以用命名管道FIFO完成。命名管道是一种特殊类型的文件,因为Linux中所有事物都是文件,它在文件系统中以文件名的形式存在。

在程序中,我们可以使用两个不同的函数调用来建立管道:

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
int mknode(const char *filename, mode_t mode | S_IFIFO, (dev_t) 0 );

下面先来创建一个管道:

#include <stdio.h>  
#include <stdlib.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
int main()  
{  
    int res = mkfifo("/tmp/my_fifo", 0777);  
    if (res == 0)  
    {  
        printf("FIFO created/n");  
    }  
    exit(EXIT_SUCCESS);  
}  

编译并运行程序,再用ls命令查看所创建的管道:

注意:ls命令的输出结果中的第一个字符为p,表示这是一个管道。最后的|符号是由ls命令的-F选项添加的,它也表示是这是一个管道。

虽然,我们所设置的文件创建模式为“0777”,但它被用户掩码(umask)设置(022)给改变了,这与普通文件创建是一样的,所以文件的最终模式为755。

打开FIFO一个主要的限制是,程序不能是O_RDWR模式打开FIFO文件进行读写操作,这样做的后果未明确定义。这个限制是有道理的,因为我们使用FIFO只是为了单身传递数据,所以没有必要使用O_RDWR模式。如果一个管道以读/写方式打开FIFO,进程就会从这个管道读回它自己的输出。如果确实需要在程序之间双向传递数据,最好使用一对FIFO,一个方向使用一个。

当一个Linux进程被阻塞时,它并不消耗CPU资源,这种进程的同步方式对CPU而言是非常有效率的。

chapter2  消息队列

1.消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式。进程可以向中按照一定的规则添加新消息;另一些进程则可以从消息队列中读走消息。

2.消息队列这一通信机制将在内核中开辟的用于保存消息的链表作为竞争的共享资源为有通信需求的多进程公用。消息来源为发送方进程,队列中已有的消息将由指定进程接收。

3.基本思想:

①在内存的操作系统空间设置一组消息缓冲区,用于暂存发送的消息;

②当发送进程要向接收进程发送消息时,首先在自己的内存空间设置一个发送区,将要发送的消息填入其中,并填入消息长度以及本进程标识符等信息,然后调用发送原语 send;

③执行send原语将产生异常,自陷系统;

④内核接收控制权后,则为需要发送的消息分配一个空缓冲区,并将要发送的消息从发送进程的发送区拷贝到其中,然后将该缓冲区链接到接收进程的消息(缓冲)队列上,至此完成消息发送过程;

⑤接收进程在本进程的内存空间设置一个接收区,在接收消息时,通过执行接收原语 receive直接从自己的消息(缓冲)队列上取下第一个消息缓冲区,并将其内容复制到接收区,然后释放该消息缓冲区的空间,至此完成消息接收过程。 

3.在队列中,可以把消息看作一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读取消息。这样一来,发送方进程不必等待接收进程检查所发消息就可以继续工作;而接收方进程如果没有收到消息也不需要等待。 

4.与管道相比,消息队列提供了有格式的数据,这可以减少开发人员的工作量。

5.消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。  每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。Linux用宏MSGMAX和MSGMNB来限制一条消息的最大长度和一个队列的最大长度。

6.在Linux中使用消息队列:Linux提供了一系列消息队列的函数接口来让我们方便地使用它来实现进程间的通信。它的用法与其他两个System V PIC机制,即信号量和共享内存相似。
(1)msgget函数:该函数用来创建和访问一个消息队列。它的原型为:
int msgget(key_t, key, int msgflg);  
与其他的IPC机制一样,程序必须提供一个键来命名某个特定的消息队列。msgflg是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg可以与IPC_CREAT做或操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被忽略,而只返回一个标识符。
它返回一个以key命名的消息队列的标识符(非零整数),失败时返回-1.
(2)msgsnd函数:该函数用来把消息添加到消息队列中。它的原型为:
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);  
msgid是由msgget函数返回的消息队列标识符。
msg_ptr是一个指向准备发送消息的指针,但是消息的数据结构却有一定的要求,指针msg_ptr所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定消息的类型。所以消息结构要定义成这样:
struct my_message{  
    long int message_type;  
    /* The data you wish to transfer*/  
};  
msg_sz是msg_ptr指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说msg_sz是不包括长整型消息类型成员变量的长度。
msgflg用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。
如果调用成功,消息数据的一分副本将被放到消息队列中,并返回0,失败时返回-1.
(2)msgrcv函数:该函数用来从一个消息队列获取消息,它的原型为
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);  
msgid, msg_ptr, msg_st的作用也函数msgsnd函数的一样。
msgtype可以实现一种简单的接收优先级。如果msgtype为0,就获取队列中的第一个消息。如果它的值大于零,将获取具有相同消息类型的第一个信息。如果它小于零,就获取类型等于或小于msgtype的绝对值的第一个消息。
msgflg用于控制当队列中没有相应类型的消息可以接收时将发生的事情。
调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的缓存区中,然后删除消息队列中的对应消息。失败时返回-1.
(4)msgctl函数:该函数用来控制消息队列,它与共享内存的shmctl函数相似,它的原型为:
int msgctl(int msgid, int command, struct msgid_ds *buf);  
command是将要采取的动作,它可以取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;  
};  
成功时返回0,失败时返回-1.
7.使用消息队列进行进程间通信
由于可以让不相关的进程进行行通信,所以我们在这里将会编写两个程序,msgreceive和msgsned来表示接收和发送信息。根据正常的情况,我们允许两个程序都可以创建消息,但只有接收者在接收完最后一个消息之后,它才把它删除。
接收信息的程序源文件为msg1.c的源代码为:
#include <unistd.h>  
#include <stdlib.h>  
#include <stdio.h>  
#include <string.h>  
#include <errno.h>  
#include <sys/msg.h>  
  
struct msg_st  
{  
    long int msg_type;  
    char text[BUFSIZ];  
};  
  
int main()  
{  
    int running = 1;  
    int msgid = -1;  
    struct msg_st data;  
    long int msgtype = 0; //注意1  
  
    //建立消息队列  
    msgid = msgget((key_t)1234, 0666 | IPC_CREAT);  
    if(msgid == -1)  
    {  
        fprintf(stderr, "msgget failed with error: %d\n", errno);  
        exit(EXIT_FAILURE);  
    }  
    //从队列中获取消息,直到遇到end消息为止  
    while(running)  
    {  
        if(msgrcv(msgid, (void*)&data, BUFSIZ, msgtype, 0) == -1)  
        {  
            fprintf(stderr, "msgrcv failed with errno: %d\n", errno);  
            exit(EXIT_FAILURE);  
        }  
        printf("You wrote: %s\n",data.text);  
        //遇到end结束  
        if(strncmp(data.text, "end", 3) == 0)  
            running = 0;  
    }  
    //删除消息队列  
    if(msgctl(msgid, IPC_RMID, 0) == -1)  
    {  
        fprintf(stderr, "msgctl(IPC_RMID) failed\n");  
        exit(EXIT_FAILURE);  
    }  
    exit(EXIT_SUCCESS);  
}  
发送信息的程序的源文件msg2.c的源代码为:
#include <unistd.h>  
#include <stdlib.h>  
#include <stdio.h>  
#include <string.h>  
#include <sys/msg.h>  
#include <errno.h>  
  
#define MAX_TEXT 512  
struct msg_st  
{  
    long int msg_type;  
    char text[MAX_TEXT];  
};  
  
int main()  
{  
    int running = 1;  
    struct msg_st data;  
    char buffer[BUFSIZ];  
    int msgid = -1;  
  
    //建立消息队列  
    msgid = msgget((key_t)1234, 0666 | IPC_CREAT);  
    if(msgid == -1)  
    {  
        fprintf(stderr, "msgget failed with error: %d\n", errno);  
        exit(EXIT_FAILURE);  
    }  
  
    //向消息队列中写消息,直到写入end  
    while(running)  
    {  
        //输入数据  
        printf("Enter some text: ");  
        fgets(buffer, BUFSIZ, stdin);  
        data.msg_type = 1;    //注意2  
        strcpy(data.text, buffer);  
        //向队列发送数据  
        if(msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1)  
        {  
            fprintf(stderr, "msgsnd failed\n");  
            exit(EXIT_FAILURE);  
        }  
        //输入end结束输入  
        if(strncmp(buffer, "end", 3) == 0)  
            running = 0;  
        sleep(1);  
    }  
    exit(EXIT_SUCCESS);  
}  
 
分析:消息类型
注意msg1.c文件main函数中定义的变量msgtype(注释为注意1),它作为msgrcv函数的接收信息类型参数的值,其值为0,表示获取队列中第一个可用的消息。再来看看msg2.c文件中while循环中的语句data.msg_type = 1(注释为注意2),它用来设置发送的信息的信息类型,即其发送的信息的类型为1。所以程序msg1能够接收到程序msg2发送的信息。
如果把注意1,即msg1.c文件main函数中的语句由long int msgtype = 0;改变为long int msgtype = 2;会发生什么情况,msg1将不能接收到程序msg2发送的信息。因为在调用msgrcv函数时,如果msgtype(第四个参数)大于零,则将只获取具有相同消息类型的第一个消息,修改后获取的消息类型为2,而msg2发送的消息类型为1,所以不能被msg1程序接收。重新编译msg1.c文件并再次执行,发现msg1并没有接收到信息和输出,而且当msg2输入end结束后,msg1也没有结束,通过jobs命令我们可以看到它还在后台运行着。
8.消息队列与命名管道的比较
消息队列跟命名管道有不少的相同之处,通过与命名管道一样,消息队列进行通信的进程可以是不相关的进程,同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write,接收数据用read,则在消息队列中,发送数据用msgsnd,接收数据用msgrcv。而且它们对每个数据都有一个最大长度的限制。
与命名管道相比,消息队列的优势在于,1、消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。2、同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。3、接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。

chapter3 共享内存

1.共享内存是被多个进程共享的一部分物理内存。共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。

2.共享内存机制允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,数据不需要在进程间复制,所以这是最快的一种IPC,适用于通信数据量较大的场合。 

3.使用共享内存时,多个进程之间对已给定的存储区需进程同步访问。即若一个进程正在将数据放入共享存储区,则在它做完这一操作 之前,其他进程不应当去读取这些数据。通常,信号量被用来实现对共 享存储的访问。

4.采用共享内存通信的一个好处是效率高,因为进程可以直接读写 内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式, 则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝 两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出 文件。

5.一般而言,进程之间在共享内存时,并不总是读写少量数据后就解 除映射,有新的通信时再重新建立共享内存区域;而是保持共享区域, 直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写 回文件。共享内存中的内容往往是解除映射时才写回文件的,因此,采 用共享内存的通信方式效率非常高。

6.实现机制:与信号量一样,在Linux中也提供了一组函数接口用于使用共享内存,而且使用共享共存的接口还与信号量的非常相似,而且比使用信号量的接口来得简单。它们声明在头文件 sys/shm.h中。
(1)shmget函数:该函数用来创建共享内存,它的原型为:
int shmget(key_t key, size_t size, int shmflg);  
第一个参数,与信号量的semget函数一样,程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1.
不相关的进程可以通过该函数的返回值访问同一共享内存,它代表程序可能要使用的某个资源,程序对所有共享内存的访问都是间接的,程序先通过调用shmget函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget函数的返回值),只有shmget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。
第二个参数,size以字节为单位指定需要共享的内存容量
第三个参数,shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。
(2)shmat函数:第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。它的原型如下:
void *shmat(int shm_id, const void *shm_addr, int shmflg);  
第一个参数,shm_id是由shmget函数返回的共享内存标识。
第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
第三个参数,shm_flg是一组标志位,通常为0。
调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.
 
(3)shmdt函数:该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。它的原型如下:
int shmdt(const void *shmaddr);  
参数shmaddr是shmat函数返回的地址指针,调用成功时返回0,失败时返回-1.
(4)shmctl函数:与信号量的semctl函数一样,用来控制共享内存,它的原型如下:
int shmctl(int shm_id, int command, struct shmid_ds *buf);  
第一个参数,shm_id是shmget函数返回的共享内存标识符。
第二个参数,command是要采取的操作,它可以取下面的三个值 :
    IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
    IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
    IPC_RMID:删除共享内存段
第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。
shmid_ds结构至少包括以下成员:
struct shmid_ds  
{  
    uid_t shm_perm.uid;  
    uid_t shm_perm.gid;  
    mode_t shm_perm.mode;  
};  
7.使用共享内存进行进程间通信:以两个不相关的进程来说明进程间如何通过共享内存来进行通信。其中一个文件shmread.c创建共享内存,并读取其中的信息,另一个文件shmwrite.c向共享内存中写入数据。为了方便操作和数据结构的统一,为这两个文件定义了相同的数据结构,定义在文件shmdata.c中。结构shared_use_st中的written作为一个可读或可写的标志,非0:表示可读,0表示可写,text则是内存中的文件。
shmdata.h的源代码如下:
#ifndef _SHMDATA_H_HEADER  
#define _SHMDATA_H_HEADER  
  
#define TEXT_SZ 2048  
  
struct shared_use_st  
{  
    int written;//作为一个标志,非0:表示可读,0表示可写  
    char text[TEXT_SZ];//记录写入和读取的文本  
};  
  
#endif  
源文件shmread.c的源代码如下:
 
#include <unistd.h>  
#include <string.h> #include <stdlib.h> #include <stdio.h> #include <sys/shm.h> #include "shmdata.h" int main() { int running = 1;//程序是否继续运行的标志 void *shm = NULL;//分配的共享内存的原始首地址 struct shared_use_st *shared;//指向shm int shmid;//共享内存标识符 //创建共享内存 shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT); if(shmid == -1) { fprintf(stderr, "shmget failed\n"); exit(EXIT_FAILURE); } //将共享内存连接到当前进程的地址空间 shm = shmat(shmid, 0, 0); if(shm == (void*)-1) { fprintf(stderr, "shmat failed\n"); exit(EXIT_FAILURE); } printf("\nMemory attached at %X\n", (int)shm); //设置共享内存 shared = (struct shared_use_st*)shm; shared->written = 0; while(running)//读取共享内存中的数据 { //没有进程向共享内存定数据有数据可读取 if(shared->written != 0) { printf("You wrote: %s", shared->text); sleep(rand() % 3); //读取完数据,设置written使共享内存段可写 shared->written = 0; //输入了end,退出循环(程序) if(strncmp(shared->text, "end", 3) == 0) running = 0; } else//有其他进程在写数据,不能读取数据 sleep(1); } //把共享内存从当前进程中分离 if(shmdt(shm) == -1) { fprintf(stderr, "shmdt failed\n"); exit(EXIT_FAILURE); } //删除共享内存 if(shmctl(shmid, IPC_RMID, 0) == -1) { fprintf(stderr, "shmctl(IPC_RMID) failed\n"); exit(EXIT_FAILURE); } exit(EXIT_SUCCESS); }
 
源文件shmwrite.c的源代码如下:
#include <unistd.h>  
#include <stdlib.h>  
#include <stdio.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 + 1];//用于保存输入的文本  
    int shmid;  
    //创建共享内存  
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);  
    if(shmid == -1)  
    {  
        fprintf(stderr, "shmget failed\n");  
        exit(EXIT_FAILURE);  
    }  
    //将共享内存连接到当前进程的地址空间  
    shm = shmat(shmid, (void*)0, 0);  
    if(shm == (void*)-1)  
    {  
        fprintf(stderr, "shmat failed\n");  
        exit(EXIT_FAILURE);  
    }  
    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: ");  
        fgets(buffer, BUFSIZ, stdin);  
        strncpy(shared->text, buffer, TEXT_SZ);  
        //写完数据,设置written使共享内存段可读  
        shared->written = 1;  
        //输入了end,退出循环(程序)  
        if(strncmp(buffer, "end", 3) == 0)  
            running = 0;  
    }  
    //把共享内存从当前进程中分离  
    if(shmdt(shm) == -1)  
    {  
        fprintf(stderr, "shmdt failed\n");  
        exit(EXIT_FAILURE);  
    }  
    sleep(2);  
    exit(EXIT_SUCCESS);  
}  
分析:
1、程序shmread创建共享内存,然后将它连接到自己的地址空间。在共享内存的开始处使用了一个结构struct_use_st。该结构中有个标志written,当共享内存中有其他进程向它写入数据时,共享内存中的written被设置为0,程序等待。当它不为0时,表示没有进程对共享内存写入数据,程序就从共享内存中读取数据并输出,然后重置设置共享内存中的written为0,即让其可被shmwrite进程写入数据。
2、程序shmwrite取得共享内存并连接到自己的地址空间中。检查共享内存中的written,是否为0,若不是,表示共享内存中的数据还没有被完,则等待其他进程读取完成,并提示用户等待。若共享内存的written为0,表示没有其他进程对共享内存进行读取,则提示用户输入文本,并再次设置共享内存中的written为1,表示写完成,其他进程可对共享内存进行读操作。
8.关于前面的例子的安全性讨论
这个程序是不安全的,当有多个程序同时向共享内存中读写数据时,问题就会出现。可能你会认为,可以改变一下written的使用方式,例如,只有当written为0时进程才可以向共享内存写入数据,而当一个进程只有在written不为0时才能对其进行读取,同时把written进行加1操作,读取完后进行减1操作。这就有点像文件锁中的读写锁的功能。咋看之下,它似乎能行得通。但是这都不是原子操作,所以这种做法是行不能的。试想当written为0时,如果有两个进程同时访问共享内存,它们就会发现written为0,于是两个进程都对其进行写操作,显然不行。当written为1时,有两个进程同时对共享内存进行读操作时也是如些,当这两个进程都读取完是,written就变成了-1.
要想让程序安全地执行,就要有一种进程同步的进制,保证在进入临界区的操作是原子操作。例如,可以使用前面所讲的信号量来进行进程的同步。因为信号量的操作都是原子性的。

chapter4 信号量

1.信号量(又名:信号灯)与其他进程间通信方式不大相同,主要用途是保护临界资源。进程可以根据它判定是否能够访问某些共享资源。除了用于访问控制外,还可用于进程同步。

2.类型:①二值信号灯:信号灯的值只能取0或1,类似于互斥锁。 但两者有不同:信号灯强调共享资源,只要共享资源可用,其他进程同样可以修改信号灯的值;互斥锁更强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。

②计数信号灯:信号灯的值可以取任意非负值。

3。信号量用来结合其他通信方式实现进程通信的同步与互斥。在操 作系统中,信号量代表了一类物理资源,它是相应物理资源的抽象。

4.信号量在创建时需要设置一个初始值,表示同时能有几个进程访 问该信号量保护的共享资源,初始值为 1 就变成互斥锁(Mutex),即同 时只能有一个进程能访问信号量保护的共享资源。

5.一个进程要想访问共享资源,首先必须得到信号量,获取信号量的 操作将把信号量的值减 1,若当前信号量的值为负数,表明无法获得信 号量,该进程必须挂起在该信号量的等待队列,等待该资源可用;若当 前信号量的值为非负数,表示能获得信号量,因而能即时访问被该信号量保护的共享资源。当进程访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加 1 实现,如果信号量的值为非正数,表明有 任务等待当前信号量,因此它也唤醒所有等待该信号量的进程。

6.Linux 中的信号量表现为一个二元组 sem(v,p),其中 v 为信号量 sem 的值,p 为最近一次因申请共享资源而被阻塞的进程的进程号。在 用信号量控制进程互斥或同步时,使用 semge(t )来获取或创建一个信 号量集;通过 semcnt(l )来控制指定的信号量集,可以为其赋初值,进行 各种设置或删除该信号量集;而在使用过程中对信号量值的修改由 semop()来完成,即通过该函数实现资源的获取或释放。

7.由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.
举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。
8.Linux的信号量机制
Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,但这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件sys/sem.h中。
(1)semget函数:它的作用是创建一个新信号量或取得一个已有信号量,原型为:
int semget(key_t key, int num_sems, int sem_flags);  
第一个参数key是整数值(唯一非零),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。
第二个参数num_sems指定需要的信号量数目,它的值几乎总是1。
第三个参数sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
semget函数成功返回一个相应信号标识符(非零),失败返回-1.
(2)semop函数:用于改变信号量的值,原型为:
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);  
sem_id是由semget返回的信号量标识符,sembuf结构的定义如下:
struct sembuf{  
    short sem_num;//除非使用一组信号量,否则它为0  
    short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,  
                 //一个是+1,即V(发送信号)操作。  
    short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,  
                  //并在进程没有释放该信号量而终止时,操作系统释放信号量  
};  
(3)semctl函数:用来直接控制信号量信息,它的原型为:
int semctl(int sem_id, int sem_num, int command, ...);  
如果有第四个参数,它通常是一个union semum结构,定义如下:
union semun{  
    int val;  
    struct semid_ds *buf;  
    unsigned short *arry;  
};  
前两个参数与前面一个函数中的一样,command通常是下面两个值中的其中一个:
SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。
9.案例:进程间的资源竞争
两个相同的进程,同时向stdout中输出字符,只是没有使用信号量,两个进程在互相竞争stdout。normalprint.c的代码如下:
#include <stdio.h>  
#include <stdlib.h>  
  
int main(int argc, char *argv[])  
{  
    char message = 'X';  
    int i = 0;    
    if(argc > 1)  
        message = argv[1][0];  
    for(i = 0; i < 10; ++i)  
    {  
        printf("%c", message);  
        fflush(stdout);  
        sleep(rand() % 3);  
        printf("%c", message);  
        fflush(stdout);  
        sleep(rand() % 2);  
    } 
    sleep(10);  
    printf("\n%d - finished\n", getpid());  
    exit(EXIT_SUCCESS);  
}  
运行结果如下:
例子分析:
当第一个进程A输出了字符后,调用sleep休眠时,另一个进程B立即输出并休眠,而进程A醒来时,再继续执行输出,同样的进程B也是如此。所以输出的字符就是不成对的出现。这两个进程在竞争stdout这一共同的资源。通过两个例子的对比,我想信号量的意义和使用应该比较清楚了。

chapter5  各种通信方式的比较

1.Linux 环境下通信机制众多,但各种通信方式都有其适用的场合。

管道是Linux支持的最初Unix IPC机制之一,是实现方法最简单的一种通信机制。但是,只能以半双工的形式在进程间进行通信。 信号是多种通信机制中唯一一种异步方式进行通信的机制。信号方式通信传输的数据量较少,侧重于控制进程根据不同的信号触发不同的行为。

消息队列是在内核中开辟的一组链表,以队列的形式接收和发送信息,适用于传输数据量较少的场合。消息队列与管道通信相比,其优势是对每一个消息可以指定特定消息类型,接收的时候不需要按队列 次序,而是可以根据自定义条件接收特定类型的消息。但在消息信息的发送进程—操作系统内核和内核—接收进程间复制时需要额外占用 CPU 的时间。

共享内存通信机制在进程间可以传送大量的数据,并且由于读写进程均将共享内存块视为本进程所有,可直接读写,因此传输速度最快。但由于多个进程对共享内存块的访问必须以互斥形式进行,因此 还需要信号量机制予以配合。

信号量机制通过信号量值的变化来控制多进程互斥的访问共享资源,或者协调多个进程并发执行的节奏,并未在进程之间实际的传输数 据。

各种机制都有其优缺点,在选择进程间通信机制时,程序设计者应根据问题本身的情况来选择合适的方式。 

 2.比较:

chapter6   研究心得

这次的项目是对进程间通信的研究,信号量的部分在操作系统中大家都已经理解学习了,因此大家一定也都有一定的功底,另外在《深入理解计算机系统》一书的第十二章中也有专门一个小节在介绍这个模块,内容充实详细,代码材料也很全。而管道,在信息安全系统设计基础第一堂课就提过了,虽说只是对简单用法的熟悉,但是对原理也有了解。这次的研究,我们加入了对命名管道的查询,也对FIFO的概念有所普及。我觉得在后面共享内存和消息队列这部分还是理解起来有些难度的。这部分内容有些抽象,概念也比较多。对底层理解比较高,代码示例也很复杂,经过我们小组的资料查找和代码运行理解,有了一些眉目,但是还是需要更多地时间和资料来完善。我们还接触了娄老师所提供的视频进行补充,最后总结出这一片博客。内容很多,也很抽象,需要慢慢地理解和学习。很难用三言两语说清楚。因此,如果对操作系统的学习有较高的要求,可以在这篇博客的框架基础上再多多扩展学习。

chapter7   参考资料

1.http://m.blog.chinaunix.net/uid-26833883-id-3227144.html 

2.《Linux高性能服务器编程》

3.http://blog.csdn.net/ljianhui/article/details/10243617

4.http://blog.csdn.net/ljianhui/article/details/10287879

5.各方式详解http://www.cnblogs.com/skyofbitbit/p/3651750.html

 

posted @ 2015-11-26 00:27  20135313吴子怡  阅读(728)  评论(0编辑  收藏  举报