Linux学习:进程间通信

内容介绍

IPC通信中的内存映射通信,管道映射通信,消息队列通信以及信号通信。

1. 实现内存映射通信。

内存映射概念
使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是相比其他通信机制运行效率较低设计的。往往与其它通信机制,如信号量结合使用, 来达到进程间的同步及互斥。

主要代码
单工通信代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/mman.h>

struct mymsg
{
    int type;
    char text[512];
};

int main()
{
    pid_t result;
    mymsg *my_map;
    char temp;

    my_map = (mymsg* )mmap(NULL, sizeof(mymsg)*10, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
    result = fork();
    if(result<0)
    {
        perror("创建子程序失败\n");
        exit(0);
    }

    else if(result == 0)
    {
        printf("子进程 %d, 开始读出数据\n", getpid());
        //sleep(4);
        for(int i=0; i<5; i++)
        {
            printf("子进程读取:第 %d 个数据是: %s \n", i, (*(my_map+i)).text );
        }
        printf("子进程读出数据结束\n");
        munmap(my_map, sizeof(mymsg)*10);
        printf("子进程解除内存映射\n");
        //exit(0);
    }
    else
    {
        int status;

        printf("父进程 %d, 开始写入数据\n", getpid());
        temp = 'a'-1;

        for(int i=0; i<5; i++)
        {
            temp += 1;
            memcpy((*(my_map+i)).text, &temp, 1);
            (*(my_map+i)).type = i;
        }
        //sleep(5);
        printf("父进程写入数据结束\n");
        printf("父进程解除内存映射\n\n");
        munmap(my_map, sizeof(mymsg)*10);

        if(waitpid(-1, &status, 0) > 0)
        {
            printf("\nThe end!\n");
        }

    }

    return 0;

}

双工通信代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#define SIZE 1024
int main()
{
    int shmid ;
    char *shmaddr ;  //共享内存地址
    struct shmid_ds buf ;
    int flag = 0 ;
    int pid ;

    shmid = shmget(IPC_PRIVATE, SIZE, IPC_CREAT|0600 ) ; //获得共享内存地址

    if ( shmid < 0 )
    {
        perror("获取共享内存失败\n") ;
        return -1 ;
    }

    pid = fork() ;
    if(pid < 0)
    {
        perror("创建子进程失败\n");
        shmctl(shmid, IPC_RMID, NULL) ;
        exit(1);
    }

    if ( pid == 0 )
    {
        //子进程
        shmaddr = (char *)shmat( shmid, NULL, 0 ) ; //映射共享内存
        if ( (long)shmaddr == -1 )
        {
            perror("共享内存地址失效\n") ;
            return -1 ;

        }
        printf("子进程开始写入数据\n");
        strcpy( shmaddr, "这一行是来自子进程的数据\n") ;
        printf("子进程写入数据完成\n\n");

        shmdt( shmaddr ) ; //撤销内存映射
        return  0;

    }
    else if ( pid > 0)
    {
        printf("父进程挂起等待子进程结束\n\n");
        //父进程
        int status;
        if(waitpid(-1, &status, 0) < 0) exit(1);    //子进程返回出错

        flag = shmctl( shmid, IPC_STAT, &buf) ; //获得控制
        if ( flag == -1 )
        {
            perror("获得控制失败\n") ;
            return -1 ;
        }

        printf("共享内存大小 %d bytes\n", buf.shm_segsz ) ;
        printf("父进程 pid=%d, shm_cpid = %d \n", getpid(), buf.shm_cpid ) ;
        printf("子进程 pid=%d, shm_lpid = %d \n",getpid() , buf.shm_lpid ) ;

        shmaddr = (char *) shmat(shmid, NULL, 0 ) ; //内存映射
        if ( (long)shmaddr == -1 )
        {
            perror("内存映射失败\n") ;
            return -1 ;
        }

        printf("父进程读出内容为: %s\n", shmaddr) ;

        shmdt( shmaddr ) ;  //解除映射
        shmctl(shmid, IPC_RMID, NULL) ;
    }


    return 0 ;
}

结果
在这里插入图片描述

图1:内存映射1

在这里插入图片描述

图2:内存映射2

结果分析:
在该实验中,我设计了两个子程序,其实两者是完全独立的。

代码1实现的是单工通信,父进程只能发送数据,然后子进程逐一读出。实现的办法是在程序中通过fork()获取进程的pid,判断是子进程还是父进程。然后采取不同的操作,一个写入,一个读出。难度不大。实验结果如图1所示。

代码2中我寻求的是双向通信,即子进程也可以发送消息给父进程。但是就存在一个问题了。父进程可以创建子进程,然后阻塞,知道子进程完成被唤醒。但是相反则不能。没有机制可以使得子进程阻塞,等待父进程完成任务之后唤醒子进程。因为这涉及到先有父进程后有子进程的问题。所以这里的代码2实现了父进程创建,并创建子进程,然后等待子进程输入,输入结束后,父进程读出数据。

2. 实现管道映射通信。

管道通信概念
管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。

主要代码

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t result;
    int r_num;
    int pipe_fd[2];
    char buf_r[100], buf_w[100];  //读写的缓冲区
    memset(buf_r, 0, sizeof(buf_r));  //将写的缓冲区初始化

    if(pipe(pipe_fd) < 0)  //创建一个管道
    {
        printf("创建管道失败\n");
        return -1;
    }
    result = fork(); //创建子进程
    if(result < 0)
    {
        perror("创建子进程失败\n");
        exit(0);
    }
    else if(result ==0)
    {
        //对于子进程
        close(pipe_fd[1]);  //关闭写通道
        if((r_num = read(pipe_fd[0],buf_r, 100)) > 0)
            printf("子进程从管道中读取 %d 个字符,读取的字符串是 : %s\n", r_num, buf_r);
        close(pipe_fd[0]);  //关闭读通道
        exit(0);
    }
    else
    {
        //对于父进程
        close(pipe_fd[0]);  //关闭读通道
        printf("输入写入管道字符串\n");
        scanf("%s", buf_w);
        if(write(pipe_fd[1], buf_w, strlen(buf_w))!= -1)
            printf("父进程向管道写入:%s\n",buf_w);
        close(pipe_fd[1]);  //关闭写通道
        waitpid(result, NULL, 0);   //调用waitpid, 阻塞父进程,等待子进程退出
        exit(0);
    }
}

实验结果
在这里插入图片描述

图3:管道通信结果

结果分析
其实本实验实现的是有名管道,可以在父进程和子进程之间进行通信。这里的机制是使用一个数组来表示管道,第一个位表示读通道。第二个位表示写通道。然后在父进程中,先关闭读通道,不让读,然后写入数据在管道中。接着关闭写通道。然后会挂起,等待子进程活动退出再退出。

在子进程中先关闭写通道,不让写,然后读出数据,接着关闭读通道,打印出数据,返回。最后返回父进程,程序结束。实验结果如图3所示。

3. 实现消息队列通信。

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

主要代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <sys/wait.h>

struct mymsg
{
    long msg_type;  //消息类型
    char msg_text[512];

};

int main()
{
    int qid;
    key_t key;
    int len;
    struct mymsg msg;
    pid_t result;

    //创建子进程
    result = fork();
    if(result < 0)
    {
        perror("创建子进程失败\n");
        exit(0);
    }

    //调用ftok创建key
    if((key=ftok(".",10))==-1)
    {
        perror("产生标准key出错\n");
        exit(1);
    }
    printf("key= %d\n", key);

    //调用msgget创建、打开消息队列
    if((qid=msgget(key, IPC_CREAT|0666)) ==-1)
    {
        perror("创建消息队列出错\n");
        exit(1);
    }

    printf("进程%d 创建、打开的队列号是:%d\n", getpid(), qid);  //消息队列准备完毕


    if(result==0)
    {//对于子进程
        if((msgrcv(qid, &msg, 512, 0, 0)) < 0)
        {
            perror("子进程读取消息出错\n");
            exit(1);
        }
        printf("子进程读取的消息是:%s \n", (&msg)->msg_text);
    }
    else
    {//对于父进程
        int status; //子进程状态
        sleep(2);
        puts("在父进程中输入要加入队列的消息:");
        if((fgets((&msg)->msg_text, 512, stdin))==NULL)
        {
            puts("没有消息");
            exit(1);
        }

        msg.msg_type = getpid();
        len = strlen(msg.msg_text);

        if((msgsnd(qid, &msg, len, 0)) < 0)
        {
            perror("父进程添加消息出错\n");
            exit(1);
        }


        if(waitpid(-1, &status, 0) > 0)
        {//等待子进程结束再关闭
            if(WIFEXITED(status)!=0)
            {//返回状态正餐

                //删除消息队列
                if((msgctl(qid, IPC_RMID, NULL)) < 0)
                {
                    printf("%d 删除消息队列出错\n", getpid());
                    exit(1);
                }
            }

        }

    }



    return 0;
}

结果
在这里插入图片描述

图4:消息队列通信结果

结果分析
从实验结果图中可以发现,父进程和子进程在创建的时候都使用相同的队列号。个人觉得这里的队列其实有点类似于共享存储通信。因为我实现的方式是采用了一个结构,里面包含了一个消息类型的标识和一块缓冲区用于存储数据。只不过在每次发送消息的时候,都有一个msgsnd()函数用于添加信息。Msgrcv()用于接受信息。这一点有点类似于信件投递。

struct mymsg
{
    long msg_type;  //消息类型
    char msg_text[512];

};

其实消息类型的标识使用的是进程号。在父进程中主要做两件事,一是写入消息,采用msgsnd发送出消息,等待子进程读取数据完成之后删除队列。
子进程则是接受消息,并且打印。实验结果如图4所示。

4. 实现信号通信。

信号映射概念:
信号是比较复杂的通信方式,用于通知接受进程有某种事件生,除了用于进程间通信外,进程还可以发送信号给进程本身。

主要代码

#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
static bool flag = true;
void handler(int sig)
{
    if(SIGALRM == sig)
    {
        printf("alarm, I got signal %d\n", sig);
    }
    else
    {
        printf("quit, I got signal %d\n", sig); flag = false; //这让子进程退出循环
    }
}
int main()
{
    printf("SIGALRM的值是 %d \t SIGQUIT的值是 %d\n", SIGALRM, SIGQUIT);
    pid_t pid;
    pid = fork();

    if(pid < 0)
    {//进程创建失败
        printf("fork() error\n");
    }
    else if(pid > 0)
    {
        printf("父进程执行到这里然后开始睡眠\n");
        sleep(8);
        kill(pid, SIGQUIT); exit(0);
        printf("没有执行这里\n");
    }

    //子进程执行部分
    struct sigaction act;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGALRM, &act, 0);
    sigaction(SIGQUIT, &act, 0);

    while(flag)
    {
        alarm(2);
        printf("子进程执行到这里然后停下等待接受信号\n");
        pause(); //此函数用于将进程挂起直到捕捉到信号为止,这个函数很常用,通常用于判断信号是否已到
    }
    printf("子进程结束\n");
    return 0;
}

结果
在这里插入图片描述

图5:信号通信结果

结果分析
这个实验其实是相对比较难理解的,所以我打印出了SIGALRM的值和SIGQUIT的值,这两者都是头文件中定义的常量。用于标识获取了消息还是退出。很神奇的一个地方是,信号量采用一个结构体sigaction用于通信。这两个常量需要在sigaction中注册。

在代码中使用了pause()函数,这个函数用于将进程挂起直到捕捉到信号为止,这个函数很常用,通常用于判断信号是否已到。

在父进程中,父进程创建子进程,然后睡眠8秒钟。而在子进程中,每两秒会发送和接受一个信号,所以打印出了“子进程执行…”,当父进程结束的时候,子进程也结束,所以此时打印出退出的提示。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值