进程间通信的总结

进程间通信常见的有5种渠道:编程

管道、信号量、共享内存、消息队列、套接字

下面来一一简单说明:segmentfault


管道

  • 管道是最简单方便的一种进程间通信的方式,它本质上是一个fifo文件。又能够分为有名管道和无名管道两种,实质上两种管道构成没有区别,可是有名管道是用户可见的管道,能够在程序中指明管道文件对其操做,而无名管道则是由系统建立,对于用户来讲是透明的,因此通常来讲无名管道只能用来对于有亲缘关系的父子进程之间的通讯,而pipe[0]默认是读端,而pipe[1]默认是写端。
  • Linux下建立管道文件的命令是: mkfifo (管道文件名)
  • 管道文件是直接写入内存的,故而管道文件是没有大小的,可是管道确实有大小的,通常是64KB大小。
  • 管道是一种半双工的通信方式,一方写入,一方读出,它在64KB范围内循环写入,写完一次后会再次返回开始继续写入直至一直没有数据读出致使写满。
  • 这里指出双向管道socketpair()并非管道!!!它的本质是两个套接字,因此会放在套接字部分描述。

两个简单的实例:


#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<fcntl.h>
#include<string.h>
#include<signal.h>

//a.c
void fun(int sig)
{
    printf("sig=%d\n", sig);
}

int main()
{
    signal(SIGPIPE, fun);

    int fdw = open("./fifo", O_WRONLY);
    assert(fdw != -1);

    printf("fdw=%d\n", fdw);

    int newfdw = dup(fdw);
    char buff[128] = {0};

//    dup2(fdw, 1);
//    printf("hello");

    while(1)
    {
        printf("input:\n");
        fgets(buff, 128, stdin);
        putchar('\n');

        if(strncmp(buff, "end", 3) == 0)
        {
            break;
        }

        write(newfdw, buff, strlen(buff));
    }

    close(fdw);
    close(newfdw);
}

//b.c
int main()
{
    int fdr = open("./fifo", O_RDONLY);
    assert(fdr != -1);

    printf("fdr=%d\n", fdr);

    char buff[128] = {0};

    while(1)
    {
        memset(buff, 0, 128);
        int len = read(fdr, buff, 127);

        if(len == 0)
        {
            break;
        }

        printf("len=%d, buff=%s\n", len, buff);
    }

    close(fdr);
}

管道文件的使用和普通文件是同样的,一方读取,一方写入,不过在a.c当中有两个有意思的函数,dup();和dup2();在这个例子中,dup()用newfdw表明了fdw,而dup2()的做用是用fdw去替换标准输出,有兴趣不妨试试,是两个有趣的函数,这里很少赘述。数组

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
int main()
{
    int fd[2];
    pipe(fd);
    
    pid_t pid = fork();
    assert(pid != -1);
    
    if(pid == 0)
    {
        close(fd[0]);
        char buff[128] = {0};
        while(1)
        {
            printf("input:\n");
            fgets(buff, 128, stdin);
        
            if(strncmp(buff, "end", 3) == 0)
            {
                break;
            }
    
            write(fd[1], buff, 127);
            sleep(1);
        }
        close(fd[1]);
    }
    else
    {
        close(fd[1]);
        char buff[128] = {0};
        while(1)
        {
            read(fd[0], buff, 127);
            printf("buff=%s\n", buff);
        }
        close(fd[0]);
    }    
}

无名管道是由系统建立的,用户并不知道它姓甚名谁,也就难以在其余进程中使用了,但在父子进程中,由于子进程会继承父进程的文件信息,故而在子进程中仍然存在着无名管道,但在使用中通常防止出错,乙方负责读取时必须关闭写端,一方负责写入时必须关闭读端缓存

另外管道的最大长度经过测试可得最大为64K,可是这并不许确,这个大小应该能够经过修改Linux下的内核参数来修改最大长度。网络

信号量

  • 信号量通常用来去同步进程,它能够控制程序推动的速度。
  • 信号量是一种特殊的变量,它通常只有两种操做————加1、减一,而这两个操做都是原子操做,是不可中断的,当信号量的值为0时,减一操做会被阻塞,也是由于如此它才能够控制同步进程,一样的,也所以信号量必定是一个大于等于0的值。
  • 通常加一操做也叫v操做,它会让信号量加一(释放资源),减一操做对应叫p操做,会让信号量减一(获取资源)。p操做可能会致使阻塞。

同时在这里咱们能够引入一下原子操做,通常原子操做是系统调用中会实现,且原子操做会消耗很大的系统资源,而信号量机制也能够用于实现原子操做。
贴上3个概念:
临界资源:同一时刻只容许一个进程访问的资源
临界区:访问临界资源的代码段
原子操做:是一种不可分割不可中断的操做多线程

信号量机制也是实现原子操做一种方式,这些都是为了去保证在多进程、多线程环境下的内存可见性问题,可是一样的,这种机制对于计算机资源形成了很大的负担,这也就产生了CAS操做,https://segmentfault.com/a/11...
关于CAS操做能够参考一下这篇文章,这里很少赘述了。dom

原子操做利用信号量机制实现的思路

思路其实很简单,咱们只须要分辨出临界区——————必定要注意就是那个同一时间下只能有一个线程去访问占有资源的代码处,在临界区的上面进行p操做,在临界区下面则进行v操做便可。socket

简单实现一个信号量控制:

//sem.h
#include<stdio.h>
#include<sys/sem.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>

union semun
{
    int val;
};

void sem_init();
void sem_p();
void sem_v();
void sem_destroy();

//sem.c
#include"sem.h"

static semid = -1;
void sem_init()
{
    semid = semget((key_t)1234,1,IPC_CREAT | IPC_EXCL | 0600);

    if(semid == -1)
    {
        //已经存在,或者系统资源不足
        //处理已经存在
        semid = semget((key_t)1234, 1, 0600);
        if(semid == -1)
        {
            //确实系统资源不足
            perror("semget error\n");
        }
    }
    else
    {
        union semun a;
        a.val = 1;
        //a.val = 0;
        if(semctl(semid, 0, SETVAL, a) == -1)
        {
            perror("semctl error\n");
        }
    }
}

void sem_p()
{
    struct sembuf buf;
    buf.sem_num = 0;//下标为0
    buf.sem_op = -1;//p
    buf.sem_flg = SEM_UNDO;//异常结束会将所作操做复原

    if(semop(semid, &buf, 1) == -1)
    {
        perror("sem_p error\n");
    }
}

void sem_v()
{
    struct sembuf buf;
    buf.sem_num = 0;//下标为0
    buf.sem_op = 1;//v
    buf.sem_flg = SEM_UNDO;//异常结束会将所作操做复原

    if(semop(semid, &buf, 1) == -1)
    {
        perror("sem_p errori\n");
    }
}

void sem_destroy()
{
    if(semctl(semid, 0, IPC_RMID) == -1)
    {
        perror("destroy error\n");
    }
}

//sema.c
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<unistd.h>
#include<string.h>
#include<sys/sem.h>
#include"sem.h"

int main()
{
    sem_init();
    int i;
    for(i = 0; i < 10; i++)
    {
        sem_p();
        write(1,"A",1);
        int n = rand() % 3;
        sleep(n);
        write(1,"A",1);
        sem_v();
        n = rand() % 3;
        sleep(n);
    }

    return 0;
}

这里面出现了几个系统调用:
在#include<sys/sem.h>中:函数

int semget(key_t key, int nsems, int semflg); 
int semctl(int semid, int semnum, int cmd,...); 
int semop(int semid, struct sembuf *sops, unsigned nsops);
//内部的成员结构:
union semun
{
    short val;//SETVAL用的值
    struct semid_ds *buf;//IPC_STAT、IPC_SET用的semid_ds结构
    unsigned short *array;//SETALL、GETALL用的数组值
    struct seminfo *buf;//为控制IPC_INFO提供的缓存
};

struct sembuf
{
    short semnum;//信号量集合中的信号量编号,0表明第一个信号量
    short val;//进行P/V操做所加减的值
    short flag;
    /*
    0设置信号量的默认操做
    IPC_NOWAIT设置信号量操做不等待
    SEM_UNDO会让内核记录一个与调用进程相关的UNDO记录,若是该进程崩溃,则根据这个进程的UNDO记录自动回复相应信号量的计数值
    */
}
semctl的cmd选项列表
IPC_STAT
从信号量集上检索semid_ds结构,并存到semun联合体参数的成员buf的地址中

IPC_SET
设置一个信号量集合的semid_ds结构中ipc_perm域的值,并从semun的buf中取出值post

IPC_RMID
从内核中删除信号量集合

GETALL
从信号量集合中得到全部信号量的值,并把其整数值存到semun联合体成员的一个指针数组中

GETNCNT
返回当前等待资源的进程个数

GETPID
返回最后一个执行系统调用semop()进程的PID

GETVAL
返回信号量集合内单个信号量的值

GETZCNT
返回当前等待100%资源利用的进程个数

SETALL
用联合体中val成员的值设置信号量集合中所有信号量的值

SETVAL
用联合体中val成员的值设置信号量集合中单个信号量的值

关于这几个系统调用的详情可参考于http://blog.csdn.net/guoping1...,对应可理解上面的例子。

另外库函数里提供了一个信号量机制实现,能够方便咱们简单调用来实行信号量操做。

在#include<semaphore.h>中:

sem_init(sem_t *sem, int pshared, unsigned int value);
pshared 参数指明信号量是由进程内线程共享,仍是由进程之间共享。若是 pshared 的值为 0,那么信号量将被进程内的线程共享,而且应该放置在这个进程的全部线程均可见的地址上(如全局变量,或者堆上动态分配的变量)。若是 pshared 是非零值,那么信号量将在进程之间共享,而且应该定位共享内存区域。
sem_wait(sem_t *sem);//对应P操做
sem_post(sem_t *sem);//对应V操做
//实例1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<unistd.h>
#include<semaphore.h>
#include<pthread.h>

static sem_t sem;
static pthread_mutex_t mutex;

void *fun(void *arg)
{
    int i;
    for(i = 0; i < 10; i++)
    {
        sem_wait(&sem);
        //pthread_mutex_lock(&mutex);

        printf("B");
        fflush(stdout);
        sleep(1);
        printf("B");
        fflush(stdout);

        sem_post(&sem);
        //pthread_mutex_unlock(&mutex);
        sleep(1);
    }
    
    pthread_exit("fun exit\n");
}

int main()
{
//    sem_init(&sem, 0, 1);
    pthread_mutex_init(&mutex, NULL);

    pthread_t id;
    pthread_create(&id, NULL, fun, NULL);

    int i;
    for(i = 0; i < 10; i++)
    {
    //    sem_wait(&sem);
        pthread_mutex_lock(&mutex);

        printf("A");
        fflush(stdout);
        sleep(1);
        printf("A");
        fflush(stdout);

    //    sem_post(&sem);
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
    printf("main over\n");

    char *s = NULL;
    pthread_join(id, (void **)&s);
    printf("%s", s);

    exit(0);
}

//实例2
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
#include<semaphore.h>

sem_t sem;

void *fun(void *arg)
{
    char *buff = (char *)arg;
    sem_wait(&sem);
    char *temp = NULL;
    char *token = NULL;
    token = strtok_r(buff, " ", &temp);
    printf("fun token=%s\n", token);
    sleep(1);
    while((token = strtok_r(NULL, " ", &temp)) != NULL)
    {
        printf("fun token=%s\n", token);
        sleep(1);
    }
    sem_post(&sem);
}

int main()
{
    char main_buf[] = "1 2 3 4 5 6 7 8 9 10";
    char fun_buf[] = "A B C D E F G H I J";

    //sem
    sem_init(&sem, 0, 1);

    pthread_t id;
    pthread_create(&id, NULL, fun, (void *)fun_buf);

    //strtok(char *, " ")
    sem_wait(&sem);
    char *token = NULL;
    char *temp = NULL;
    token = strtok_r(main_buf, " ", &temp);
    printf("main token=%s\n", token);
    sleep(1);
    while((token = strtok_r(NULL, " ", &temp)) != NULL)
    {
        printf("main token=%s\n", token);
        sleep(1);
    }
    sem_post(&sem);

    pthread_join(id,NULL);
    exit(0);
}

上面的例子中还用到了互斥锁,也就是那个mutex,对应两个函数pthread_mutex_lock(pthread_mutex_t mutex),pthread_mutex_unlock(pthread_mutex_t mutex),和信号量相似,也是在临界区上方加锁下方解锁,这里下次再说吧这个。

共享内存

  • 共享内存也是做为一种进程间通信的方式,思路上讲和管道类似,就是在内存上申请一份空间让进程共享这片空间,相似于给进程AB两我的一块儿买了一块秘密基地吧,道理上于管道大同小异,可是它是进程间通信最简单的一种通信方式,它容许两个进程同时享用一片内存,因此它在通信时有最高的效率,就像访问进程本身的空间同样快捷方便,不须要陷入内核态或者系统调用。
  • 管道、消息队列等在实现上须要进数据去拷贝在内核当中,再由内核拷贝到接收方,可是共享内存不同,它的实如今效率上实现的最大化,它只用输入------》共享内存区,共享内存区-----》接收,两步完成,没有内核的拷贝,效率得以提高。
  • 共享内存容许两个或更多进程访问同一块内存,就如同 malloc() 函数向不一样进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。
  • 由于系统内核没有对访问共享内存进行同步,因此必须提供本身的同步措施。例如,在数据被写入以前不容许进程从共享内存中读取信息、不容许两个进程同时向同一个共享内存地址写入数据等。解决这些问题的经常使用方法是经过使用信号量进行同步,而信号量见上。

来看看具体的实现吧:

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

int main()
{
    sem_init();

    int shmid = shmget((key_t)1234, 256, IPC_CREAT | 0600);
    assert(shmid != -1);

    char *s = (char *)shmat(shmid, NULL, 0);//NULL不指定进程连接中的地址具体位置,0即便标识位,为默认)
    assert((int) s != -1);
    //assert(s != (char *)-1);
      
    while(1)
    {
        sem_p(0);

        printf("input str\n");
        char buff[128] = {0};
        fgets(buff, 128, stdin);

        strcpy(s, buff);
        
        sem_v(1);
        if(strncmp(buff, "end", 3) == 0)
        {
            break;
        }
    }
    shmdt(s);//断开连接s
    exit(0);
}

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

int main()
{
    sem_init();

    int shmid = shmget((key_t)1234, 256, IPC_CREAT | 0600);
    assert(shmid != -1);

    char *s = (char *)shmat(shmid, NULL, 0);//NULL不指定进程连接中的地址具体位置,0即便标识位,为默认)
    assert((int) s != -1);
    //assert(s != (char *)-1);
    
    while(1)
    {
        sem_p(1);
        if(strncmp(s, "end", 3) == 0)
        {
            break;
        }
        printf("read:%s\n", s);
        sleep(1);
        sem_v(0);
    }

    shmdt(s);//断开连接s
    sem_destroy();
    exit(0);
}

这里关于信号量的不在多提,在上面也说到,由于系统内核没有对访问共享内存进行同步,因此必须提供本身的同步措施,因此会用到信号量。而且这里用的信号量是自行实现的,关于实如今前面的实例中有展开。

在头文件#include<sys/shm.h>中:
int shmget(ket_t key, size_t size, int shmflg);
void shmat(int shmid, const void shmaddr, int shmflg);
int shmdt(const void *shmaddr);

  • 关于shmflg:
  • 0----取共享内存标识符,若不存在函数会报错
  • IPC_CREAT----当shmflg&IPC_CREAT为真时,若是内核中不存在键值为key的共享内存,则新建一个共享内存,若是存在则返回该共享内存标识符。
  • IPC_CREAT|IPC_EXCL----若是内核中不存在键值为key的共享内存则新建,若是存在则报错

详情可参见:http://blog.csdn.net/guoping1...

消息队列

  • 消息队列也属于进程间通信的一种方式,它于管道相似,但少了打开和关闭管道方面的复杂性。使用消息队列并未解决咱们在使用命名管道时遇到的一些问题,如管道满时的阻塞问题。消息队列提供了一种在两个不相关进程间传递数据的简单有效的方法。与命名管道相比:消息队列的优点在于,它独立于发送和接收进程而存在,这消除了在同步命名管道的打开和关闭时可能产生的一些困难。消息队列提供了一种从一个进程向另外一个进程发送一个数据块的方法。并且,每一个数据块被认为含有一个类型,接收进程能够独立地接收含有不一样类型值的数据块。
  • 优势:

    A. 咱们能够经过发送消息来几乎彻底避免命名管道的同步和阻塞问题。
     B. 咱们能够用一些方法来提早查看紧急消息。
  • 缺点:

    A. 与管道同样,每一个数据块有一个最大长度的限制。
     B. 系统中全部队列所包含的所有数据块的总长度也有一个上限。
  • Linux系统中有两个宏定义:
    MSGMAX, 以字节为单位,定义了一条消息的最大长度。
    MSGMNB, 以字节为单位,定义了一个队列的最大长度。
  • 限制:

    因为消息缓冲机制中所使用的缓冲区为共用缓冲区,所以使用消息缓冲机制传送数据时,两通讯进程必须知足
     以下条件:
    (1)在发送进程把写入消息的缓冲区挂入消息队列时,应禁止其余进程对消息队列的访问,不然,将引发消
         息队列的混乱。同理,当接收进程正从消息队列中取消息时,也应禁止其余进程对该队列的访问。
    (2)当缓冲区中无消息存在时,接收进程不能接收任何消息;而发送进程是否能够发送消息,则只由发送进
         程是否可以申请缓冲区决定。

再来看看具体实现:

//msga
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<unistd.h>
#include<memory.h>
#include<sys/msg.h>

typedef struct my_message
{
    long int type;
    char msg[128];
}MyMsg;

int main()
{
    int msgid = msgget((key_t)1234, IPC_CREAT | 0600);
    if(msgid == -1)
    {
        perror("a msgget error\n");
    }

    char buff[128];
    int type;
    MyMsg m;
    while(1)
    {
        puts("input type(int)");
        scanf("%d", &type);
        getchar();
    //    fflush(stdin);
        m.type = type;
        
        memset(buff, 0, 128);
        puts("input message");
        fgets(buff, 127, stdin);
        buff[strlen(buff) - 1] = 0;
        strcpy(m.msg, buff);

        if((msgsnd(msgid, &m, sizeof(MyMsg) - sizeof(long), IPC_NOWAIT)) == -1)
        {
            perror("a msgsnd error\n");
        }
        
        if(strncmp(buff, "end", 3) == 0)
        {
            break;
        }
    }
    exit(0);
}

//msgb
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<unistd.h>
#include<memory.h>
#include<sys/msg.h>

typedef struct my_message
{
    long int type;
    char msg[128];
}MyMsg;

int main()
{
    int msgid = msgget((key_t)1234, IPC_CREAT | 0600);
    if(msgid == -1)
    {
        perror("b msgget error\n");
    }

    char buff[128];
    int type;
    MyMsg m;
    int i = 0;
    while(1)
    {
        puts("output type(int)");
        scanf("%d", &type);
        fflush(stdin);
        m.type = type;
        
        memset(buff, 0, 128);
        if(msgrcv(msgid, &m, sizeof(MyMsg) - sizeof(long), type, IPC_NOWAIT) == -1)
        {
            perror("b msgrcv error\n");
            continue;
        }
        strcpy(buff, m.msg);
        printf("buff[%d]=%s\n", i++, buff);

        if(strncmp(buff, "end", 3) == 0)
        {
            break;
        }
    }
    msgctl(msgid, IPC_RMID, NULL);
    exit(0);
}

在#inlcude<sys/msg.h>中:

  • int msgget(key_t key, int msgflg);
  • 关于msgflg:
    0----取消息队列标识符,若不存在函数会报错
    IPC_CREAT----当msgflg&IPC_CREAT为真时,若是内核中不存在键值为key的共享内存,则新建一个消息队列,若是存在则返回该共享内存标识符。
    IPC_CREAT|IPC_EXCL----若是内核中不存在键值为key的消息队列则新建,若是存在则报错
  • int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • 关于cmd:
    IPC_STAT:得到msgid的消息队列第一个消息到buf中。
    IPC_SET:设置消息队列的属性,要设置的属性须要存在buf中。
  • 关于buf结构:

图片描述

  • int msgsnd(int msgid, const void *msgp, size_t msgsz, int msgflg);
    这其中的msgsz是不包含消息类型的,也就是说消息结构体大小须要减去一个long类型长度的大小!
  • 关于msgflg:
    0----当消息队列满时,msgsnd会阻塞,知道消息能写入。
    IPC_NOWAIT:当消息队列为满时,msgsnd函数不等待当即返回。
    IPC_NOERROR:若发送消息大于size字节,则直接截断发送不通知。
  • ssize_t msgrcv(int msgid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
  • 关于msgflg:
    0----阻塞式接收消息,没有改类型的消息则msgrcv一直阻塞。
    IPC_NOWAIT:当消息队列为满时,msgrcv函数不等待当即返回,错误码:ENOMSG。
    IPC_EXCEPT:与msgtype配合试用返回队列中第一个类型不为msgtype的消息。
    IPC_NOERROR:若发送消息大于size字节,则直接截断接收不通知。
  • 关于struct msgbuf
    struct msgbuf{

    long mtype; //这是一个消息中必须包含的消息类型,要求为long
      ……………………    //具体的消息

    };

详情可参见:http://blog.csdn.net/guoping1...

套接字

套接字是网络编程的基础,是进程间通信中一种经常使用的通信方式,按照Linux下一切皆文件的思想,那么套接字就能够看做是一个文件的描述符。这个部分更多的偏向于网络编程,延伸至select,poll,epoll乃至libevent、boost::asio等等网络库,这个放在后面再讨论吧。
在这里额外提一下双向管道好了,双向管道,这是一种全双工的通信方式,双方均可读可写,那么这也就是普通管道不能知足需求的缘由,因此诞生了一种以套接字为基础的轻量级别的解决方案,socketpair();
在#include<sys/socket.h>中:
int socketpair(int domain, int type, int protocol, int sv[2]);
domain参数:选择协议族 AF_UNIX,AF_LOCAL;
type参数: 选择类型:SOCK_STREAM,SOCK_DGRAM
pritocal 必须为0!

这样处理后,sv数组中会放有两个链接好的套接字文件,这样经过两个套接字就能够互通有无了。

参考资料:
http://blog.csdn.net/Zong__Zo...
http://blog.csdn.net/guoping1...
http://blog.csdn.net/guoping1...
http://blog.csdn.net/guoping1...
http://blog.csdn.net/ttyue_12...

相关文章
相关标签/搜索