20155219 第十周课下作业-IPC

时间:2021-03-31 18:52:40

题目:研究Linux下IPC机制:原理,优缺点,每种机制至少给一个示例,提交研究博客的链接

共享内存
管道
FIFO
信号
消息队列

1.共享内存

共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言函数malloc分配的内存一样。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

  • 共享内存的使用与信号量一样,在Linux中也提供了一组函数接口用于使用共享内存,而且使用共享共存的接口还与信号量的非常相似,而且比使用信号量的接口来得简单。它们声明在头文件 sys/shm.h中。

  • 共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。所以我们通常需要用其他的机制来同步对共享内存的访问,例如前面说到的信号量。关于信号量可以再次想读我们的教材进行学习。

shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。

void *shmat(int shm_id, const void *shm_addr, int shmflg);  

第一个参数,shm_id是由shmget函数返回的共享内存标识。

第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。

第三个参数,shm_flg是一组标志位,通常为0。

调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.

shmdt函数
该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。

int shmdt(const void *shmaddr);  

参数shmaddr是shmat函数返回的地址指针,调用成功时返回0,失败时返回-1.

shmctl函数
与信号量的semctl函数一样,用来控制共享内存。

int shmctl(int shm_id, int command, struct shmid_ds *buf); 

其中包含的数据结构如下:

struct shmid_ds  
{  
    uid_t shm_perm.uid;  
    uid_t shm_perm.gid;  
    mode_t shm_perm.mode;  
};  

第一个参数,shm_id是shmget函数返回的共享内存标识符。

第二个参数,command是要采取的操作,它可以取下面的三个值 :
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
IPC_RMID:删除共享内存段

第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。

管道

管道允许在进程之间按先进先出的方式传送数据,是进程间通信的一种常见方式。

管道是Linux 支持的最初Unix IPC形式之一,具有以下特点:

1) 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;

2) 匿名管道只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);

3) 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。

管道分为pipe(无名管道)和fifo(命名管道)两种,除了建立、打开、删除的方式不同外,这两种管道几乎是一样的。他们都是通过内核缓冲区实现数据传输。

pipe用于相关进程之间的通信,例如父进程和子进程,它通过pipe()系统调用来创建并打开,当最后一个使用它的进程关闭对他的引用时,pipe将自动撤销。
FIFO即命名管道,在磁盘上有对应的节点,但没有数据块——换言之,只是拥有一个名字和相应的访问权限,通过mknode()系统调用或者mkfifo()函数来建立的。一旦建立,任何进程都可以通过文件名将其打开和进行读写,而不局限于父子进程,当然前提是进程对FIFO有适当的访问权。当不再被进程使用时,FIFO在内存中释放,但磁盘节点仍然存在。
管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据:管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后再缓冲区都不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。

Linux中通过系统调用mknod()或makefifo()来创建一个命名管道。最简单的方式是通过直接使用shell

mkfifo myfifo

等价于

mknod myfifo p

例子
用命名管道实现聊天程序,一个张三端,一个李四端。两个程序都建立两个命名管道,fifo1,fifo2,张三写fifo1,李四读fifo1;李四写fifo2,张三读fifo2。

用select把,管道描述符和stdin假如集合,用select进行阻塞,如果有i/o的时候唤醒进程。(粉红色部分为select部分,黄色部分为命名管道部分)

20155219 第十周课下作业-IPC

FIFO

  • 无名管道:
    pipe的例子:父进程创建管道,并在管道中写入数据,而子进程从管道读出数据
  • 命名管道:
    和无名管道的主要区别在于,命名管道有一个名字,命名管道的名字对应于一个磁盘索引节点,有了这个文件名,任何进程有相应的权限都可以对它进行访问。

而无名管道却不同,进程只能访问自己或祖先创建的管道,而不能访任意访问已经存在的管道——因为没有名字。

  • 创建一个管道:
#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);  
}  

如下图所示:
20155219 第十周课下作业-IPC

例子如下
生产者程序:

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <fcntl.h>  
#include <limits.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
  
#define FIFO_NAME "/tmp/Linux/my_fifo"  
#define BUFFER_SIZE PIPE_BUF  
#define TEN_MEG (1024 * 1024 * 10)  
  
int main()  
{  
    int pipe_fd;  
    int res;  
    int open_mode = O_WRONLY;  
  
    int bytes = 0;  
    char buffer[BUFFER_SIZE + 1];  
  
    if (access(FIFO_NAME, F_OK) == -1)  
    {  
        res = mkfifo(FIFO_NAME, 0777);  
        if (res != 0)  
        {  
            fprintf(stderr, "Could not create fifo %s/n", FIFO_NAME);  
            exit(EXIT_FAILURE);  
        }  
    }  
  
    printf("Process %d opening FIFO O_WRONLY/n", getpid());  
    pipe_fd = open(FIFO_NAME, open_mode);  
    printf("Process %d result %d/n", getpid(), pipe_fd);  
  
    if (pipe_fd != -1)  
    {  
        while (bytes < TEN_MEG)  
        {  
            res = write(pipe_fd, buffer, BUFFER_SIZE);  
            if (res == -1)  
            {  
                fprintf(stderr, "Write error on pipe/n");  
                exit(EXIT_FAILURE);  
            }  
            bytes += res;  
        }  
        close(pipe_fd);  
    }  
    else  
    {  
        exit(EXIT_FAILURE);  
    }  
  
    printf("Process %d finish/n", getpid());  
    exit(EXIT_SUCCESS);  
}  

消费者程序

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <fcntl.h>  
#include <limits.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
  
#define FIFO_NAME "/tmp/Linux/my_fifo"  
#define BUFFER_SIZE PIPE_BUF  
  
int main()  
{  
    int pipe_fd;  
    int res;  
  
    int open_mode = O_RDONLY;  
    char buffer[BUFFER_SIZE + 1];  
    int bytes = 0;  
  
    memset(buffer, '/0', sizeof(buffer));  
  
    printf("Process %d opeining FIFO O_RDONLY/n", getpid());  
    pipe_fd = open(FIFO_NAME, open_mode);  
    printf("Process %d result %d/n", getpid(), pipe_fd);  
  
    if (pipe_fd != -1)  
    {  
        do{  
            res = read(pipe_fd, buffer, BUFFER_SIZE);  
            bytes += res;  
        }while(res > 0);  
        close(pipe_fd);  
    }  
    else  
    {  
        exit(EXIT_FAILURE);  
    }  
  
    printf("Process %d finished, %d bytes read/n", getpid(), bytes);  
    exit(EXIT_SUCCESS);  
}  

消息队列

消息队列,就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。

消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。

可以把消息看做一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程可以从消息队列中读取消息。

消息队列的常用函数如下表:
20155219 第十周课下作业-IPC

进程间通过消息队列通信,主要是:创建或打开消息队列,添加消息,读取消息和控制消息队列。

msgget函数

int msgget(key_t, key, int msgflg);  

与其他的IPC机制一样,程序必须提供一个键来命名某个特定的消息队列。msgflg是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg可以与IPC_CREAT做或操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被忽略,而只返回一个标识符。

它返回一个以key命名的消息队列的标识符(非零整数),失败时返回-1.

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.

使用消息队列进行进程间通信的例子

#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);  
}  
#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);  
}  

消息队列与命名管道的比较

消息队列跟命名管道有不少的相同之处,通过与命名管道一样,消息队列进行通信的进程可以是不相关的进程,同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write,接收数据用read,则在消息队列中,发送数据用msgsnd,接收数据用msgrcv。而且它们对每个数据都有一个最大长度的限制。

与命名管道相比,消息队列的优势在于:
1、消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。

2、同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。

3、接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。

信号

信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。

信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。

可以从两个不同的分类角度对信号进行分类:

可靠性方面:可靠信号与不可靠信号;

与时间的关系上:实时信号与非实时信号。

signal()函数

#include <signal.h>

void (*signal(int signum, void (*handler))(int)))(int);

如果该函数原型不容易理解的话,可以参考下面的分解方式来理解:

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler));

第一个参数指定信号的值,第二个参数指定针对前面信号值的处理,可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。

如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。

传递给信号处理例程的整数参数是信号值,这样可以使得一个信号处理例程处理多个信号。

 

#include <signal.h>

#include <unistd.h>

#include <stdio.h>

void sigroutine(int dunno)

{ /* 信号处理例程,其中dunno将会得到信号的值 */

        switch (dunno) {

        case 1:

        printf("Get a signal -- SIGHUP ");

        break;

        case 2:

        printf("Get a signal -- SIGINT ");

        break;

        case 3:

        printf("Get a signal -- SIGQUIT ");

        break;

        }

        return;

}

 

int main() {

        printf("process id is %d ",getpid());

        signal(SIGHUP, sigroutine); //* 下面设置三个信号的处理方法

        signal(SIGINT, sigroutine);

        signal(SIGQUIT, sigroutine);

        for (;;) ;

}

 

信号的发送
发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

kill()

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid,int signo)

该系统调用可以用来向任何进程或进程组发送任何信号。参数pid的值为信号的接收进程

pid>0 进程ID为pid的进程

pid=0 同一个进程组的进程

pid<0 pid!=-1 进程组ID为 -pid的所有进程

pid=-1 除发送进程自身外,所有进程ID大于1的进程

Sinno是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。

Kill()最常用于pid>0时的信号发送。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno。下面是一些可能返回的错误代码:

EINVAL:指定的信号sig无效。

ESRCH:参数pid指定的进程或进程组不存在。注意,在进程表项中存在的进程,可能是一个还没有被wait收回,但已经终止执行的僵死进程。

EPERM: 进程没有权力将这个信号发送到指定接收信号的进程。因为,一个进程被允许将信号发送到进程pid时,必须拥有root权力,或者是发出调用的进程的UID 或EUID与指定接收的进程的UID或保存用户ID(savedset-user-ID)相同。如果参数pid小于-1,即该信号发送给一个组,则该错误表示组中有成员进程不能接收该信号。

sigqueue()

#include <sys/types.h>

#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval val)

调用成功返回 0;否则,返回 -1。

 

sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。

sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。

typedef union sigval {

               int  sival_int;

               void *sival_ptr;

}sigval_t;

 

sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。

在调用sigqueue时,sigval_t指定的信息会拷贝到对应sig 注册的3参数信号处理函数的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。由于sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。

alarm()

#include <unistd.h>

unsigned int alarm(unsigned int seconds)

系统调用alarm安排内核为调用进程在指定的seconds秒后发出一个SIGALRM的信号。如果指定的参数seconds为0,则不再发送 SIGALRM信号。后一次设定将取消前一次的设定。该调用返回值为上次定时调用到发送之间剩余的时间,或者因为没有前一次定时调用而返回0。

注意,在使用时,alarm只设定为发送一次信号,如果要多次发送,就要多次使用alarm调用。

setitimer()
现在的系统中很多程序不再使用alarm调用,而是使用setitimer调用来设置定时器,用getitimer来得到定时器的状态,这两个调用的声明格式如下:

int getitimer(int which, struct itimerval *value);

int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

在使用这两个调用的进程中加入以下头文件:

#include <sys/time.h>

该系统调用给进程提供了三个定时器,它们各自有其独有的计时域,当其中任何一个到达,就发送一个相应的信号给进程,并使得计时器重新开始。三个计时器由参数which指定,如下所示:

TIMER_REAL:按实际时间计时,计时到达将给进程发送SIGALRM信号。

ITIMER_VIRTUAL:仅当进程执行时才进行计时。计时到达将发送SIGVTALRM信号给进程。

ITIMER_PROF:当进程执行时和系统为该进程执行动作时都计时。与ITIMER_VIR-TUAL是一对,该定时器经常用来统计进程在用户态和内核态花费的时间。计时到达将发送SIGPROF信号给进程。

定时器中的参数value用来指明定时器的时间,其结构如下:

struct itimerval {

        struct timeval it_interval; /* 下一次的取值 */

        struct timeval it_value; /* 本次的设定值 */

};

该结构中timeval结构定义如下:

struct timeval {

        long tv_sec; /* 秒 */

        long tv_usec; /* 微秒,1秒 = 1000000 微秒*/

};

在setitimer 调用中,参数ovalue如果不为空,则其中保留的是上次调用设定的值。定时器将it_value递减到0时,产生一个信号,并将it_value的值设定为it_interval的值,然后重新开始计时,如此往复。当it_value设定为0时,计时器停止,或者当它计时到期,而it_interval 为0时停止。调用成功时,返回0;错误时,返回-1,并设置相应的错误代码errno:

EFAULT:参数value或ovalue是无效的指针。

EINVAL:参数which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一个。

下面是关于setitimer调用的一个简单示范,在该例子中,每隔一秒发出一个SIGALRM,每隔0.5秒发出一个SIGVTALRM信号:


#include <signal.h>

#include <unistd.h>

#include <stdio.h>

#include <sys/time.h>

int sec;

 

void sigroutine(int signo) {

        switch (signo) {

        case SIGALRM:

        printf("Catch a signal -- SIGALRM ");

        break;

        case SIGVTALRM:

        printf("Catch a signal -- SIGVTALRM ");

        break;

        }

        return;

}

 

int main()

{

        struct itimerval value,ovalue,value2;

        sec = 5;

 

        printf("process id is %d ",getpid());

        signal(SIGALRM, sigroutine);

        signal(SIGVTALRM, sigroutine);

 

        value.it_value.tv_sec = 1;

        value.it_value.tv_usec = 0;

        value.it_interval.tv_sec = 1;

        value.it_interval.tv_usec = 0;

        setitimer(ITIMER_REAL, &value, &ovalue);

 

        value2.it_value.tv_sec = 0;

        value2.it_value.tv_usec = 500000;

        value2.it_interval.tv_sec = 0;

        value2.it_interval.tv_usec = 500000;

        setitimer(ITIMER_VIRTUAL, &value2, &ovalue);

 

        for (;;) ;

}

abort()

#include <stdlib.h>

void abort(void);

向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。

** raise()**
```

include

int raise(int signo)

向进程本身发送信号,参数为即将发送的信号值。调用成功返回 0;否则,返回 -1。
```