1

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

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

下面来一一简单说明:


管道

  • 管道是最简单方便的一种进程间通讯的方式,它本质上是一个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下的内核参数来修改最大长度。

信号量

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

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

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

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

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

简单实现一个信号量控制:
//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中取出值

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...


且行且歌_C
62 声望8 粉丝

逝者如斯夫,不舍昼夜


引用和评论

0 条评论