【操作系统】Linux下进程通信(共享内存,管道,消息队列,Socket)

Linux 下进程通信

博客新址,这里更有趣
在网络课程中,有讲到Socket编程,对于tcp讲解的环节,为了加深理解,自己写了Linux下进程Socket通信,在学习的过程中,又接触到了其它的几种方式。记录一下。

共享内存通信方式

共享内存的通信方式,系统根据我的偏好设置在内存中开辟一块空间,并对其进行相应的指定,然后我们的另一个进程可以按照同样的指定,也就是其标记,对其进行访问。创建共享内存,得到共享内存后,想内存中写入数据,然后另个进程得到内存,然后从中读出数据。
先贴出代码,写入方

#include <stdlib.h>   
#include <sys/shm.h>
#include <sys/ipc.h>  
#include <unistd.h>  
#include <string.h>

#define PATH ""
#define SIZE 512
#define ID 0
int main()
{
    char * shmAddr;
    char * dataAddr = "Hello";
    int key = ftok(PATH,ID);
    int shmID = shmget(key,SIZE,IPC_CREAT);
    shmAddr = shmat(shmID,NULL,0);
    strcpy(shmAddr,dataAddr);
    shmdt(shmAddr);
    exit(0);
}

接收方

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

#define PATH ""
#define SIZE 0
#define ID 0
int main()
{
    char * shmAddr;
    char * dataAddr = "Hello";
    int key = ftok(PATH,ID);
    int shmID = shmget(key,SIZE,0);
    shmAddr = shmat(shmID,NULL,0);
    printf("%s\n",shmAddr);
    shmdt(shmAddr);
    shmctl(shmID, IPC_RMID, NULL);
    exit(0);
}

代码比较简单,不难看出,通过函数shmget(),我们得到了对一块共享内存的标记,然后通过shmdt(),然后和我们的进程绑定得到这块共享内存的地址,然后即可输出该块内存区域中的数据。主要涉及几个参数,ftok(),shmget(),shmat()。

ftok():接受两个参数一个是文件目录,一个是我们的projectid,对于第二个id,我们这里可以随便制定,此处指定0,该函数可以为当前IPC生成一个唯一的键值。
shemget():第一个参数是我们进程的键值,第二个参数是我们指定内存区域的大小,如果制定为0则代表我们只是打开,而不是去创建,第三个参数是制定我们对该内存区域的操作模式权限。
shmat:该参数的作用是将我们的共享内存映射到我们的当前进程的内存空间。得到了其地址。第一个参数是我们要映射的共享内存的标示符。第二个参数是制定将共享内存的映射到我们进程空间的起始地址,如果制定为null,则会由系统自动分配一个。第三个参数是指定我们的操作模式,是读还是写,此处指定为0,表示既可以对其读又可以对其写。
shmdt:这个函数是用来断开当前进程和该共享内存区域的连接,
shmctl:该函数通过对于一些参数的指定来对共享内存区域进行一系列的控制,对于共享内存区域,设有一个计时器,当到达一定时间之后,如果没有进程和其绑定,该内存将会被回收。也可以不用手动对其回收。

下面运行下这两个程序
《【操作系统】Linux下进程通信(共享内存,管道,消息队列,Socket)》
当我们再次运行client2后
《【操作系统】Linux下进程通信(共享内存,管道,消息队列,Socket)》
因为我们的连个进程都已经和该共享内存区域解绑,而且也执行了移除,如果我们不对其移除。则会
《【操作系统】Linux下进程通信(共享内存,管道,消息队列,Socket)》

管道通信(匿名,有名)

管道通信,在一个进程之中,只能单一的对其写或者是读,而不可以及执行写操作又执行读操作。这一点,我们可以将其想象成我们的水管,分别连着不同的两端,在有水流的时候,一端只能进行输入,另一端只能进行输出,而不可以同时输入和输出。

管道又分为有名管道和匿名管道,两者的区别在于对于匿名管道,其只能在具有亲缘关系的父子进程之间进行消息通信。管道的通信是借助于在我们的磁盘上生成一个文件,然后对文件的读写来实现我们的通信,而且数据被读之后,在文件中将会被删除掉。

匿名

匿名管道的实现较为简单,实现代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUFFER_SIZE 9

int main()
{
        int n;
        int fd[2];
        pid_t pid;
        char buf[BUFFER_SIZE];
        if(pipe(fd)==-1)
        {
                printf("error in create pipe\n");
                exit(1);
        }
        if((pid=fork())<0)
        {
                printf("error in fork a sub process\n");
                exit(1);
        }
        if(pid>0)
        {
                close(fd[0]);
                write(fd[1],"Welcome!",10);
        }
        else
        {
                close(fd[1]);
                read(fd[0],buf,9);
                printf("%s,son\n",buf);
        }
        return 0;
}

1.创建管道2.通过fork函数分叉出子进程3.写数据4.读数据5.关闭管道读端和写端。这里也除了管道函数,还要特别说一下的是fork函数。

fork函数返回的数据在父进程中是子进程pid,而对子进程返回的是0,这并不是说,子进程的pid就是零,还有一个函数是getpid(),执行这个函数后,在子进程和父进程中得到的都是该进程pid,而不是fork函数的返回值。fork函数执行的时候,是将父进程中全部的数据置入自己进程之中,和父进程中的数据不是同步的了。

通过pipe函数我们创建了一个管道,管道接收的参数是一个长度为2的一维数组,此时,此时在0位返回的是读端口,1位返回的是写端口,当我们要对数据进行写入的时候,我们需要关闭其读端口,如果对其进行读操作,我们要关闭写端口。类似于读文件的操作,制定数据和大小,然后通过read和write函数对其进行读写。

后来又发现的一些问题是对于读写端口的问题,当我们在写的时候,读端口开着对其并没有影响,其目的是以防万一,写入数据被自身读取走了。还有此处实现方式是通过建立一个管道文件,然后采用了常规的文件读写方式进行的读写,还会出现的一些是,当我们read完一次之后,如果下面接着再read,将会出现一个问题,我们还能够从中读出数据来,原因是管道文件本身具有一个自身管理的,来负责对于管道文件的擦除,而其擦除速度较慢于我们从中读出的速度,所以导致了这个问题。

《【操作系统】Linux下进程通信(共享内存,管道,消息队列,Socket)》

有名

通过对于匿名管道的分析,再到有名管道,为什么有名管道可以被非亲缘进程找到利用?因为它有名呀,对的,如果在家中,父亲要和儿子谈话,只需说出来就好了,因为信道上的接听者只有父亲和儿子,所以即使不指儿子的名字和儿子说,儿子也是知道是在和他讲,但是如果要和别人讲话,而且由很多人同时在侦听信道的时候,如果我们不指定名字,他们就不会知道我们是在跟谁讲话。所以对于有名管道,我们首先要对其指定一个名字,然后指定作为写端或者是读端,然后对其进行操作。

#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>

#define FIFO_NAME "/oswork/pipe/myfifo"

int main()
{
        int res;
        int pipe_id;
        char buffer[] = "Hello world";
        if(access(FIFO_NAME,F_OK)==-1)
        {
                res = mkfifo(FIFO_NAME,O_WRONLY);
        if(res!=0)
                {
                        printf("Error in creating fifo.\n");
                        exit(1);
                }
        }
        pipe_id = open(FIFO_NAME,O_WRONLY);
        if(pipe_id!= -1)
        {
                if(write(pipe_id,buffer,PIPE_BUF)>0){
                        close(pipe_id);
                }else{
                        printf("Error in writing.\n");
                        exit(1);
                }
        }else
        {
                printf("Error in opening.\n");
                exit(1);
        }

因为管道是通过本地磁盘上的文件进行信息的交换,因此我们需要给予其本地磁盘上的一个文件目录,然后根据该目录通过access()函数来获取该管道,如果获取失败,那么我们就按照给定的目录创建一个,创建管道的时候,我们需要制定是对其进行读还是写,创建好之后,通过指定模式打开我们的管道,此时会得到一个管道号,然后根据获得管道标志号,作为参数,进行read和write操作。下面是读端的执行代码。

#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>

#define FIFONAME "/oswork/pipe/myfifo"
int main()
{
        char buffer[PIPE_BUF+1];
        int pipe_id;
        pipe_id = open(FIFONAME,O_RDONLY);
        if(pipe_id != -1)
        {
                read(pipe_id,buffer,PIPE_BUF);
                printf("%s",buffer);
        }else{
                printf("error in reading\n");
        }
}

执行结果,在这里的读和写都是设置为堵塞执行方式,就是如果没读端,这个时候,写端就是处于堵塞状态
《【操作系统】Linux下进程通信(共享内存,管道,消息队列,Socket)》
这个时候再执行读端
《【操作系统】Linux下进程通信(共享内存,管道,消息队列,Socket)》
写端堵塞解除
《【操作系统】Linux下进程通信(共享内存,管道,消息队列,Socket)》

Socket通信

Socket通信,不仅仅是一台主机上的两个进程可以进行通信,还可以让处在因特网中的两个进程进行通信。在两个进程进行通信的时候,首先本地的进程在运行的时候会绑定一个端口,然后我们本地为该进程生成一个缓冲区,返回一个值,即为socket作为对其进行标记,每当本地进程和远程一个进程建立连接的时候,就会根据远程进程的信息和本地进程的信息生成一个socket,然后双方借助于socket就可以进行通信,运输层得到的数据写入socket标志的缓冲区,然后在里面进行相应的操作之后将其提交给网络层。相比其它的几种处理方式,该中方式比较麻烦。多于服务端,通过listen阻塞监听,监听到有连接请求,通过accept函数得到一个本地与之对应的缓冲区,然后创建一个进程用来和该连接进行交互,然后通过receive来接收信息,由于c语言大一学过去之后,基本没怎么再看过,所以写来的时候还是遇到了几个小坑。这里实现的是当连接建立后,服务端给本地端发送一个连接建立提示,然后客户端可以向服务端发送消息,服务端给予一个I don’t know的回复。

服务端代码

#include <sys/type.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>

#define PORT 9000
#define KEY 123
#define SIZE 1024


int main()
{
    char buf[100];
    memset(buf,0,100);
    int server_sockfd,client_sockfd;
    socklen_t server_len,client_len;
    struct  sockaddr_in server_sockaddr,client_sockaddr;
    /*create a socket.type is AF_INET,sock_stream*/
    server_sockfd = socket(AF_INET,SOCK_STREAM,0);
    /*the address of the sockt which is a struct include sinfamily,sin_port and sin_addr(struct)
    htons is a convert function,*/
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(PORT);
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*the length of the sever_sockt address*/
    server_len = sizeof(server_sockaddr);
    /*
    first option is socket we create,the second option is level the left is 
    the paraments'length and value
    */
    int on;
    setsockopt(server_sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
    /*bind a socket or rename a sockt*/
    if(bind(server_sockfd,(struct sockaddr*)&server_sockaddr,server_len)==-1){
        printf("bind error");
        exit(1);
    }
    /*listen the socket,the second option is 5 which is the limit number of kernal handle*/
    if(listen(server_sockfd,5)==-1){
        printf("listen error");
        exit(1);
    }

    client_len = sizeof(client_sockaddr);
    /*define a pid number*/
    pid_t ppid,pid;
    while(1){
        if(client_sockfd=accept(server_sockfd,(struct sockaddr*)&client_sockaddr,&client_len)==-1){
            printf("connect error");
            exit(1);
        }
        else{
            //send and receiv all include a socket , content and the len. 
            send(client_sockfd,"You have conected the server",strlen("You have connected the server"),0);
        }
        ppid = fork();
        if(ppid == -1) { 
             printf("fork 1 failed:"); 
         } 
         /*the subthread is used to receiver the message from client Create a process again when 
         we create father process success*/
         else if(ppid == 0){ 
             int recvbytes; 
              pid = fork(); 
          if(pid == -1){
              printf("fork 2 failed:"); 
              exit(1);
          } 
          else if(pid == 0) {
              while(1){ 
                  //set the 100 byte of the buf to 0
                  bzero(buf,100); 
                  if((recvbytes = recv(client_sockfd,buf,100,0))==-1) { 
                      perror("read client_sockfd failed:");
                  } 
                  //if the subProcess receive the message successfully
                  else if(recvbytes != 0){
                      buf[recvbytes] = '\0'; 
                      usleep(1000); 
                      printf("%s:%d said:%s\n",inet_ntoa(client_sockaddr.sin_addr), ntohs(client_sockaddr.sin_port), buf); 
                      //send same message to the client. 
                      if(send(client_sockfd,buf,recvbytes,0)==-1){ 
                          perror("send error"); 
                          break;
                      } 
                  }
                  //when send the msg successfuly end up the method 
                  } 
              } 
              else if(pid>0) {

              }
          } else if(ppid>0){ //总父进程中关闭client_sockfd(因为有另一个副本在子进程中运行了)返回等待接收消息 
                  close(client_sockfd); 
              } 
          } 
        return 0;
    }


}

其中涉及到创建进程与结束进程等还是比较复杂的。下面通过一个流程图说明其工作原理
《【操作系统】Linux下进程通信(共享内存,管道,消息队列,Socket)》
对于其中的一些函数,有自己的英文注释,想锻炼下英文表达能力,但是表达在语法和意思上还是有些错误的。
客户端工作代码

#include <stdio.h> 
#include <stdlib.h> 
#include <errno.h> 
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h> 
#include <sys/wait.h> 
#include <unistd.h> 
#include <arpa/inet.h> 

#define SERVER_PORT 9000
/* the port of the sever which will connected*/ 
#define MAXDATASIZE 100 /* the maxmum bytes every send*/ 
#define SERVER_IP "114.215.100.147" 
/* the ip address of the server*/ 

/*First we need to create a socket and then initialize the socket,then we invoke the connect,it will build a connection between client 
and server we designate*/
int main() { 
    int sockfd, numbytes; 
    char buf[MAXDATASIZE]; 
    struct sockaddr_in server_addr; 
    printf("\n======================client initialization======================\n"); 
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { 
        perror("socket"); 
        exit(1); 
    }
    server_addr.sin_family = AF_INET; 
    server_addr.sin_port = htons(SERVER_PORT); 
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); 
    bzero(&(server_addr.sin_zero),sizeof(server_addr.sin_zero)); 
    if (connect(sockfd, (struct sockaddr *)&server_addr,sizeof(struct sockaddr_in)) == -1){
         perror("connect error"); 
         exit(1);
     } 
     //waitting for receive msg from the server,the receiver method is a block method,when the server break up
     //the connection ,is will close the socket.
     while(1) { 
         bzero(buf,MAXDATASIZE); 
         printf("\nBegin receive...\n"); 
         if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1){  
             perror("recv"); 
             exit(1);
         } 
         else if (numbytes > 0){ 
             int len, bytes_sent;
             buf[numbytes] = '\0'; 
            printf("Received: %s\n",buf);
            printf("Send:"); 
            char msgb[100];
            scanf("%s",msg);
            len = strlen(msg); 
            //sent to the server
            if(send(sockfd,msg,len,0) == -1){ 
                perror("send error"); 
            }
        } 
        else { 
            printf("soket end!\n"); 
        } 
        }  
        close(sockfd); 
        return 0;
    }

相比于服务器端,客户端的代码比较简单了,发起连接请求–>连接成功进入轮询–>接受消息————>发送消息。
执行结果
《【操作系统】Linux下进程通信(共享内存,管道,消息队列,Socket)》

《【操作系统】Linux下进程通信(共享内存,管道,消息队列,Socket)》

消息队列通信

消息队列和有名管道有些类似的地方,最大相同点在于它可以用于在不同的进程之间进行通信,但是管道有一个劣势是,对于接收端其对与管道内的数据只能是接受,而不可以对其进行过滤选择,同时在写和读的时候还会出现堵塞.对于消息队列其在接收的时候也是会发生堵塞的,解除阻塞,只有当其接收到合适的消息或者该队列被删除了,这个阻塞才会解除。对于消息队列的通信流程是创建消息-》获得队列–》向队列中发送消息–》从队列中取出消息。

实现代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <sys/msg.h>
#define MSG_FILE "/oswork/message/sender.c"
#define BUFFER 255
#define PERM S_IRUSR|S_IWUSR
//message struct
struct msgbuf
{
    long mtype;
    char mtext[BUFFER+1];
};
int main()
{
    struct msgbuf msg;
    key_t key;
    int msgid;
    int i;
    char *myask="I'm receiver, 3 messages received from you.";
    //create a key for
    if((key=ftok(MSG_FILE,66))==-1)
    {
        fprintf(stderr,"Creat Key Error:%s \n",strerror(errno));
        exit(EXIT_FAILURE);
    }
    //get a message queue
    if((msgid=msgget(key,PERM|IPC_CREAT))==-1)
    {
        fprintf(stderr,"Creat MessageQueue Error:%s \n",strerror(errno));
        exit(EXIT_FAILURE);
    }
    //get a message from the queue everytime
    for(i=0; i<3; i++)
    {
        msgrcv(msgid,&msg,sizeof(struct msgbuf),1,0);
        printf("Receiver receive: %s\n",msg.mtext);
    }
    msg.mtype=2;
    //send the message that I have received the message.
    strncpy(msg.mtext,myask,BUFFER);
    msgsnd(msgid,&msg,sizeof(struct msgbuf),0);
    return 1;
}

the code of sender

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

#define MSG_FILE "/oswork/message/sender.c"
#define BUFFER_SIZE 255
#define PERM S_IRUSR|S_IWUSR

struct msgbuf
{
    long mtype;
    char mtext[BUFFER_SIZE+1];
};
//create three messages
char *message[3]={"I'm sender,there are some message for you.","Message1",
"Message2"};

int main()
{
    struct  msgbuf msg;
    key_t key;
    int msgid;
    if((key=ftok(MSG_FILE,66))==-1)
    {
        printf("error in creating key\n");
        exit(0);
    }
    if((msgid=msgget(key,PERM|IPC_CREAT))==-1)
    {
        printf("error in creating get message\n");
        exit(0);
    }
//set the type of the message
    msg.mtype=1;
    int i;
//send the message
    for(i=0; i<3; i++)
    {
        strncpy(msg.mtext,message[i],BUFFER_SIZE);
        msgsnd(msgid,&msg,sizeof(struct msgbuf),2,0);
    }
    memset(&msg,'\0',sizeof(struct msgbuf));
/*receive the message,the third arguments show the type of message will be received
*/
    msgrcv(msgid,&msg,sizeof(struct msgbuf),2,0);
    printf("%s\n",msg.mtext);
//delete the message queue.
    if(msgctl(msgid,IPC_RMID,0)==-1)
    {
        printf("error in deleting msg queue\n");
        exit(0);
    }
    return 1;
}

对于消息通信,其中的几个函数,这里在说明一下,msgrcv和msgsnd两个函数,对于其中的参数,第一个指定的是我们的消息队列的标示符,然后第二个是消息区域,然后是我们的消息长度,然后是消息类型,最后一个是用来指定是否堵塞。消息队列中的消息只要是被读出的,就会自动的被从队列中剔除掉。

上述为Linux下进程间通信的四种方式,实际开发中,我们传输数据类型和进程间的关系选择合适的方式。

前段时间在牛客网上将《剑指offer》书中算法题用Java实现了一遍,放在了Github上,欢迎一起交流,提升。https://github.com/Jensenczx/…

    原文作者:Jensen
    原文地址: https://segmentfault.com/a/1190000004061543
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞