• 进程间通信 (IPC) 方法总结 (一)


    进程间通信 (IPC) 方法总结

    进程间通信(IPC,InterProcess Communication)

    Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。

    alt

    在进程间完成数据传递需要借助操作系统提供特殊的方法,如:管道、命名管道、信号、消息队列、共享内存、信号量、套接字等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有:

    1. 管道 (使用最简单)
    2. 信号 (开销最小)
    3. 共享映射区 (无血缘关系)
    4. 本地套接字 (最稳定)

    进程间通信的7种方式

    管道/匿名管道(PIPE)

    • 管道是半双工,数据只能向一个方向流动;双方需要互相通信时,需要建立起两个管道。
    • 只能用于具有亲缘关系的进程(父子进程或者兄弟进程)之间。
    • 管道对于两端通信的进程来说就只是一种文件,一种不属于文件系统仅存在内存中的“伪文件”。
    • 管道的通信方式为:写端每次都将数据写入管道缓冲区的 末尾 ,而读端每次都从管道缓冲区的 头部 读出数据。

    管道的实质

    管道的实质是内核利用 环形队列 的数据结构在 内核缓冲区 中的一个实现,默认设置大小为4K,可以通过ulimit -a命令查看。由于利用 环形队列 进行实现,读和写的位置都是自动增长的,不能随意改变,一个数据只能被读取一次,读取后数据就会从缓冲区中移除。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或者写进程进入等待队列,当空的缓冲区有新数据写入或者满的缓冲区有数据读出来时,就唤醒等待队列中的进程继续读写。

    管道的局限

    • 由于管道采用半双工通信方式。因此,只支持单向数据流。
    • 只能由于具有亲缘关系的进程。
    • 管道的缓冲区大小有限
    • 没有名称

    管道的使用

    创建管道

    int pipe(int pipefd[2]); //成功:0;失败:-1,设置errno

    函数调用成功返回r/w两个文件描述符。无需open,但需手动close。规定:fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区

    管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?通常可以采用如下步骤:

    alt

    1. 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。
    2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
    3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。

    eg.

    #include <unistd.h>
    #include <stdio.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    
    int main(int argc, char *argv[])
    {
        int fd[2];
        int res = pipe(fd);
        if(res == -1)
        {
            perror("pipe error");
            return -1;
        }
        pid_t pid = fork();
        if(pid == 0){
            // child
            close(fd[1]);// close write
            char buf[100];
            int count = 0;
            while(count < 5)
            {
                memset(buf,0,sizeof(buf));
                int res = read(fd[0], buf, sizeof(buf));
                if(res > 0)
                {
                    buf[res - 1] = '';
                }
                printf("I am child! Recv msg:%s from father
    ", buf);
                ++count;
            }
            close(fd[0]);
        }else if(pid > 0){
            close(fd[0]);//close read
            int count = 0;
                char buf[100];
            while(count < 5)
            {
                memset(buf,0,sizeof(buf));
                sprintf(buf,"count = %d", ++count);
                int res = write(fd[1],buf,strlen(buf) + 1);// 多一个字节存放
                printf("I am father! Send msg:%s to child!
    ",buf);
                sleep(2);
            }
            close(fd[1]);
            if(waitpid(pid, NULL, 0) < 0)
            {
                return -1;
            }
        }else{
            perror("fork error!");
            return -1;
        }
    
        return 0;
    }
    
    

    运行结果:

    alt

    管道的读写行为

    使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):

    1. 如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
    2. 如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
    3. 如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。
    4. 如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。

    总结:

    • 读管道:
      1. 管道中有数据,read返回实际读到的字节数。
      2. 管道中无数据:
        • 管道写端被全部关闭,read返回0 (好像读到文件结尾)。
        • 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)。
    • 写管道:
      1. 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)。
      2. 管道读端没有全部关闭:
        • 管道已满,write阻塞。
        • 管道未满,write将数据写入,并返回实际写入的字节数。

    命名管道(FIFO)

    FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过FIFO,不相关的进程也能交换数据。

    命名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以命名管道的文件形式存在于文件系统中,这样,即使与命名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过命名管道相互通信,因此,通过命名管道不相关的进程也能交换数据。值的注意的是,命名管道严格遵循先进先出(first in first out),对匿名管道及有名管道的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。命名管道的名字存在于文件系统中,内容存放在内存中。

    匿名管道和命名管道总结

    1. 管道是特殊类型的文件,在满足先入先出的原则条件下可以进行读写,但不能进行定位读写。
    2. 匿名管道是单向的,只能在有亲缘关系的进程间通信;有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
    3. 无名管道阻塞问题:无名管道无需显示打开,创建时直接返回文件描述符,在读写时需要确定对方的存在,否则将退出。如果当前进程向无名管道的一端写数据,必须确定另一端有某一进程。如果写入无名管道的数据超过其最大值,写操作将阻塞,如果管道中没有数据,读操作将阻塞,如果管道发现另一端断开,将自动退出。
    4. 命名管道阻塞问题:命名管道在打开时需要确实对方的存在,否则将阻塞。即以读方式打开某管道,在此之前必须一个进程以写方式打开管道,否则阻塞。此外,可以以读写(O_RDWR)模式打开命名管道,即当前进程读,当前进程写,不会阻塞。

    命名管道的使用

    创建方式:

    1. 命令:mkfifo 管道名

    2. 库函数:
      int mkfifo(const char *pathname, mode_t mode); //成功:0; 失败:-1

      一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、write、unlink等。

    eg.

    fifo_w.c // 向FIFO中写入数据

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <errno.h>
    #include <string.h>
    #include <fcntl.h>
    
    #define MAX_BUF_SIZE 655360
    
    int main(int argc, char* argv[])
    {
        if(argc < 2)
        {
            fprintf(stderr,"usage: %s argv[1].
    ",argv[0]);
            return -1;
        }
        if(mkfifo(argv[1], 0666) < 0 && errno != EEXIST)
        {
            fprintf(stderr,"Fail to mkfifo %s : %s.",argv[1],strerror(errno));
            return -1;
        }
        int fd;
        if((fd = open(argv[1],O_WRONLY)) < 0)
        {
            fprintf(stderr,"Fail to open mkfifo %s : %s.",argv[1],strerror(errno));
            return -1;
        }
        printf("open for write success
    ");
        
        int n;
        char buf[MAX_BUF_SIZE];
        while(1)
        {
            memset(buf, 0, sizeof(buf));       
            printf(">");
            scanf("%s",buf);
            
            n = write(fd, buf, strlen(buf) + 1);// 将也写入
            printf("Write %d bytes.
    Send MSG:%s
    ",n, buf);
        }
    
        return 0;
    }
    
    

    fifo_r.c // 从FIFO中读取数据

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <string.h>
    #include <errno.h>
    
    #define MAX_BUF_SIZE 655360
    
    int main(int argc, char *argv[])
    {
        if(argc < 2)
        {
            fprintf(stderr,"Usage: %s argv[1]
    ",argv[0]);
            return -1;
        }
    
        if(mkfifo(argv[1],0666) < 0 && errno != EEXIST)
        {
            fprintf(stderr,"Fail to mkfifo %s : %s",argv[1],strerror(errno));
            return -1;
        }
        int fd;
        if((fd = open(argv[1],O_RDONLY)) < 0)
        {
    
            fprintf(stderr,"Fail to open mkfifo %s : %s",argv[1],strerror(errno));
            return -1;
        }
        
        int n;
        char buf[MAX_BUF_SIZE];
        while(1)
        {
            memset(buf, 0, sizeof(buf));
            n = read(fd, buf, sizeof(buf));
            printf("Read %d bytes
    RECV MSG:%s
    ",n, buf);
        }
        return 0;
    }
    
    
    

    运行结果:

    写入:

    alt

    读取:

    alt

    信号(SIGNAL)

    • 信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
    • 如果该进程当前并未处于执行状态,则该信号就有内核保存起来,直到该进程回复执行并传递给它为止。
    • 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。

    Linux系统中常用信号:

    1. SIGHUP:用户从终端注销,所有已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程。
    2. SIGINT:程序终止信号。程序运行过程中,按Ctrl+C键将产生该信号。
    3. SIGQUIT:程序退出信号。程序运行过程中,按Ctrl+键将产生该信号。
    4. SIGBUS和SIGSEGV:进程访问非法地址。
    5. SIGFPE:运算中出现致命错误,如除零操作、数据溢出等。
    6. SIGKILL:用户终止进程执行信号。shell下执行kill -9发送该信号。
    7. SIGTERM:结束进程信号。shell下执行kill 进程pid发送该信号。
    8. SIGALRM:定时器信号。
    9. SIGCLD:子进程退出信号。如果其父进程没有忽略该信号也没有处理该信号,则子进程退出后将形成僵尸进程。
      可以使用 kill -l 查看当前系统可用信号有哪些

    信号来源

    信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,,信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源:

    • 硬件来源:用户按键输入 Ctrl+C 退出、硬件异常如无效的存储访问等。
    • 软件终止:终止进程信号、其他进程调用 kill 函数、软件异常产生信号。

    信号生命周期和处理流程

    1. 信号被某个进程产生,并设置此信号传递的对象(一般为对应进程的pid),然后传递给操作系统;
    2. 操作系统根据接收进程的设置(是否阻塞)而选择性的发送给接收者,如果接收者阻塞该信号(且该信号是可以阻塞的),操作系统将暂时保留该信号,而不传递,直到该进程解除了对此信号的阻塞(如果对应进程已经退出,则丢弃此信号),如果对应进程没有阻塞,操作系统将传递此信号。
    3. 目的进程接收到此信号后,将根据当前进程对此信号设置的预处理方式,暂时终止当前代码的执行,保护上下文(主要包括临时寄存器数据,当前程序位置以及当前CPU的状态)、转而执行中断服务程序,执行完成后在回复到中断的位置。当然,对于抢占式内核,在中断返回时还将引发新的调度。

    alt

    eg.

    借助SIGCHLD信号回收子进程

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    #include <signal.h>
    
    void sys_err(char *str)
    {
        perror(str);
        exit(1);
    }
    
    void do_sig_child(int signo)
    {
        int status;    pid_t pid;
        while ((pid = waitpid(0, &status, WNOHANG)) > 0) {
            if (WIFEXITED(status))
                printf("child %d exit %d
    ", pid, WEXITSTATUS(status));
            else if (WIFSIGNALED(status))
                printf("child %d cancel signal %d
    ", pid, WTERMSIG(status));
        }
    }
    
    int main(void)
    {
        pid_t pid;    int i;
        for (i = 0; i < 10; i++) {
            if ((pid = fork()) == 0)
                break;
            else if (pid < 0)
                sys_err("fork");
        }
        if (pid == 0) {    
            int n = 1;
            while (n--) {
                printf("child ID %d
    ", getpid());
                sleep(1);
            }
            return i+1;
        } else if (pid > 0) {
            struct sigaction act;
            act.sa_handler = do_sig_child;
            sigemptyset(&act.sa_mask);
            act.sa_flags = 0;
            sigaction(SIGCHLD, &act, NULL);
            
            while (1) {
                printf("Parent ID %d
    ", getpid());
                sleep(1);
            }
        }
        return 0;
    }
    
    

    运行结果:

    alt

    消息队列(MESSAGE)

    • 消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。
    • 与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
    • 另外与管道不同的是,消息队列在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达

    消息队列特点总结:

    1. 消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识.
    2. 消息队列允许一个或多个进程向它写入与读取消息
    3. 管道和消息队列的通信数据都是先进先出的原则。
    4. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比FIFO更有优势。
    5. 消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
    6. 目前主要有两种类型的消息队列:POSIX消息队列以及System V消息队列,System V消息队列目前被大量使用。System V消息队列是随内核持续的,只有在内核重起或者人工删除时,该消息队列才会被删除。

    消息队列的使用

    对于系统中的每个消息队列,内核维护一个定义在<sys/msg.h>头文件中的信息结构。

    struct msqid_ds {
        struct ipc_perm msg_perm ; 
        struct msg*    msg_first ; //指向队列中的第一个消息
        struct msg*    msg_last ; //指向队列中的最后一个消息
        ……
    } ;
    
    msgget函数

    调用的第一个函数通常是msgget,其功能是打开一个现存队列或创建一个新队列。

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

    返回值是一个整数标识符,其他三个msg函数就用它来指代该队列。它是基于指定的key产生的,而key既可以是ftok的返回值,也可以是常值IPC_PRIVATE。
    oflag是读写权限的组合(用于打开时)。它还可以是IPC_CREATE或IPC_CREATE | IPC_EXCL(用于创建时)。

    msgsnd函数

    使用msgsnd打开一个消息队列后,我们使用msgsnd往其上放置一个消息。

    #include <sys/msg.h>
    int  msgsnd (int msqid,  const void *ptr,  size_t length,  int flag) ;
    

    其中msqid是由msgget返回的标识符。ptr是一个结构指针,该结构具有如下模板(我们需要按这个模板自己定义结构体)

    struct mymesg {
        long  mtype ;     //消息类型(大于0)
        char  mtext[512] ;  //消息数据
    } ;
    //结构体的名字和其中变量名都由我们自己确定,我们只要按照这个模板定义即可。
    

    消息数据mtext中,任何形式的数据都是允许的,无论是二进制数据还是文本,内核根本不解释消息数据的内容。(我们可以在消息的数据部分 再分割一部分 根据需要定义自己的通信协议)

    参数length指定了待发送消息数据部分的长度。

    参数flag的值可以指定为IPC_NOWAIT。这类似于文件IO的非阻塞IO标志。若消息队列已满,则指定IPC_NOWAIT使得msgsnd立即出错返回EAGAIN。

    如果没有指定IPC_NOWAIT,则进程阻塞直到下述情况出现为止:①有空间可以容纳要发送的消息 ②从系统中删除了此队列(返回EIDRM“标识符被删除”)③捕捉到一个信号,并从信号处理程序返回(返回EINTR)

    msgrcv函数

    使用msgrcv函数从某个消息队列中读出一个消息。

    #include <sys/msg.h>
    ssize_t  msgrcv (int msqid,  void* ptr,  size_t length,  long type,  int flag) ;
    

    参数ptr指定所接收消息的存放位置。参数length指定了数据部分大小(只想要多长的数据)

    参数type指定希望从队列中读出什么样的消息。

    type == 0 返回队列中的第一个消息

    type > 0  返回队列中消息类型为type的第一个消息

    type < 0  返回队列中消息类型值小于或等于type绝对值的消息,如果这种消息有若干个。则取类型值最小的消息。

    (如果一个消息队列由多个客户进程和一个服务器进程使用,那么type字段可以用来包含客户进程的进程ID)

    参数flag可以指定为IPC_NOWAIT,使操作不阻塞。

    msgctl函数

    msgctl函数提供在一个消息队列上的各种控制操作。

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

    参数cmd说明对由msqid指定的队列要执行的命令:

    IPC_STAT :取此队列的msqid_ds结构,并将它存放在buf指向的结构中。

    IPC_SET  :按由buf指向结构中的值,设置与此队列相关结构中的字段。

    IPC_RMID:从系统中删除该消息队列以及仍在该队列中的所有数据。

    (这三条命令也可用于信号量和共享存储)

    eg.

    一个写进程,多个读进程

    //-------------------头文件msgqueue.h ------------------
    #ifndef _MAGQUEUE_H_
    #define _MAGQUEUE_H_
    #include <sys/ipc.h> //包含ftok
    #include <sys/msg.h>
    #include <sys/types.h>
     
    //消息队列的读 写模式掩码
    #define MSG_W 0200
    #define MSG_R 0400
     
    //定义众所周知的消息队列键
    #define MQ_KEY1 128L
     
    #define DATA_SIZE 512
     
    typedef struct msgbuf
    {
        long mtype ;
        char mdata[DATA_SIZE] ;
    } mymsg_t ;
     
    #endif
     
    //-----------------客户端进程-----------------
     
    #include "msgqueue.h"
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
     
    void client(int, int) ;
     
    int main(int argc, char** argv)
    {
        int  msgqid ;
     
        //打开消息队列
        msgqid = msgget(MQ_KEY1, 0) ;
        if (msgqid < 0)
        {
            puts("Open msg queue error!
    ") ;
            exit(0) ;
        }
     
        client(msgqid, msgqid) ;
        exit(0) ;
    }
     
    void client(int readfd, int writefd)
    {
        mymsg_t msgToServer ;
        mymsg_t msgFromServer ;
        char*   writePtr ;
        ssize_t pidLen ;
        ssize_t dataLen ;
        ssize_t recvBytes ;
        int     pid ;
     
        //-------构造一条消息-----
        //消息类型为1
        msgToServer.mtype = 1 ;
     
        //在消息头部放本进程ID和空格
        pid = getpid() ;
        snprintf(msgToServer.mdata, DATA_SIZE, "%ld ", pid) ;
        pidLen = strlen(msgToServer.mdata) ;
        writePtr = msgToServer.mdata + pidLen ;
     
        //从标准输入读入文件路径
        fgets(writePtr, DATA_SIZE - pidLen, stdin) ;
        dataLen = strlen(msgToServer.mdata) ;  
        if (msgToServer.mdata[dataLen-1] == '
    ') //删除换行符
        {
            msgToServer.mdata[dataLen-1] = '' ;
        }
     
        //发送消息
        if (msgsnd(writefd, &msgToServer, strlen(msgToServer.mdata), 0) == -1)
        {
            puts("Send Error!");
            exit(0) ;
        }
     
        //-----接收来自服务器的消息
        while ((recvBytes = msgrcv(readfd, &msgFromServer, DATA_SIZE, pid, 0)) > 0)
        {
            write(STDOUT_FILENO, msgFromServer.mdata, recvBytes) ;
        }
     
    }
     
    //---------------服务器端进程---------------
    //消息队列是双向通信的,故用单个队列就够用。
    //我们用每个消息的类型来标识该消息是从客户到服务器,还是从服务器到客户。
    //客户向队列发类型为1、PID和路径名。
    //服务器向队列发类型为客户进程ID的文件内容。
    //
    //小心死锁隐患:
    //客户们可以填满消息队列,妨碍服务器发送应答,于是客户被阻塞在发送中,服务器也被阻塞。
    //避免的方法是:约定服务器对消息队列总是使用非阻塞写。
     
    #include "msgqueue.h"
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
     
    void server(int, int) ;
     
    int main(int argc, char** argv)
    {
        int  msgqid;
     
        //创建消息队列
        msgqid = msgget(MQ_KEY1, IPC_CREAT) ;
        if (msgqid < 0)
        {
            puts("Create msg queue error!
    ") ;
            exit(0) ;
        }
     
        server(msgqid, msgqid) ;
        exit(0) ;
    }
     
    void server(int readfd, int writefd)
    {
        FILE*    fp ;
        pid_t    clientPid ;
        mymsg_t* msgPtr ;
        ssize_t  recvBytes ;
        char*    pathStr ;    
     
        while(1)
        {
            //从消息队列中读取来自客户的请求文件路径
            msgPtr = malloc(DATA_SIZE + sizeof(long)) ;
            recvBytes = msgrcv(readfd, msgPtr, DATA_SIZE, 1, 0) ; //阻塞读
            if (recvBytes <= 0)
            {
                puts("pathname missing") ;
                continue ;
            }
            msgPtr->mdata[recvBytes] = '' ;
     
            //分析消息,提取客户PID,文件路径
            if ((pathStr = strchr(msgPtr->mdata, ' ')) == NULL)
            {
                puts("bogus request!") ;
                continue ;
            }
            *pathStr++ = 0 ;
            clientPid = atol(msgPtr->mdata) ;
     
            //读取文件内容 返回给客户
            msgPtr->mtype = clientPid ; //msgPtr既作为接收消息 又用作发送消息
            if ((fp = fopen(pathStr, "r")) == NULL)
            {
                //读取文件失败,返回给客户失败信息(在原消息内容后 添加错误信息)
                snprintf(msgPtr->mdata + recvBytes, sizeof(msgPtr->mdata) -recvBytes, 
                        ": can't open!") ;
     
                if (msgsnd(writefd, msgPtr, strlen(msgPtr->mdata), IPC_NOWAIT) == -1)
                {
                    puts("Send Error!");
                    exit(0);
                }
            }
            else
            {   //copy文件内容 发给客户
                while (fgets(msgPtr->mdata, DATA_SIZE, fp) != NULL)
                {
                    msgsnd(writefd, msgPtr, strlen(msgPtr->mdata), IPC_NOWAIT) ; //非阻塞写
                }
            }
        }//while()
    }
    
  • 相关阅读:
    html meta标签
    随滚动条滚动,动态修改元素class
    获取浏览器长宽自动设置
    SpringMVC常用注解實例詳解2:@ModelAttribute
    SpringMVC常用注解實例詳解1:@Controller,@RequestMapping,@RequestParam,@PathVariable
    Freemarker常用指令使用范例
    Spring整合Freemarker
    SpringMVC配置入門
    再谈深浅拷贝 后端
    转发-react 性能深度探讨
  • 原文地址:https://www.cnblogs.com/joker-wz/p/11000489.html
Copyright © 2020-2023  润新知