天天看点

linux进程间通信(IPC interProcess Communication)

进程间通信常用的四种方式

  • 管道
  • 信号
  • 共享映射区
  • 本地套接字

管道

(管道可以用fcntl设置非阻塞属性)

Linux将管道视为一种特殊文件,因此可以使用问价接口来操作管道;但管道属于一种特殊文件,它没有数据块,只通过系统内存存放要传送的数据。管道中的数据只能由一端传送到另一端,因此管道被设计为环形的数据结构,如此既能实现管道的循环利用,又能方便内核对管道的管理。管道实质上是内上的一块缓冲区。

匿名管道(匿名管道只能在有亲缘关系的进程间使用)

int pipe(int fd[2]);
           
  • 管道采用半双工的通信方式,只能进行单向数据传输。虽然多余的读写端口不一定会对程序造成影响,但是为了保险起见,还是应该关闭多余的通信端口
  • 管道只能进行半双工通信,若要实现双向通信,需要为通信的进程创建两个管道
  • 只有指向读端的文件描述符打开时,向管道中写入数据才有意义,否则写端的进程会收到内核传来的SIGPIPE信号,默认情况下该信号导致进程终止
  • 若所有指向写端的文件描述符都被关闭以后仍有进程从管道的读端读取数据,那么,管道中剩余的数据都被读取以后,再次read会返回0
  • 若有指向管道写端的文件描述符没有关闭,而管道写端的进程又没有向管道中写入数据,那么当管道读端的进程从管道中读取数据完剩余的数据后,再次read会阻塞,直到写端向管道写入数据,阻塞解除
  • 若有指向管道写端的文件描述符未关闭,但读端没有从管道中读取数据某些段进程持续向管道中写入数据,当管道缓存区写满时,再次write会阻塞,直到读端将数据读出,阻塞解除

匿名管道示例

#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#include<wait.h>
#include<stdio.h>
#include<unistd.h>
int main()
{
        int fd[2];
        int ret=pipe(fd);
        if(ret==-1)
        {
                perror("pipe");
                exit(1);
        }
        ret=fork();
        if(ret==0)
        {
                char buf[20]={0};
                close(fd[1]);
                ret=read(fd[0],buf,sizeof(buf));
                close(fd[0]);
                write(STDOUT_FILENO,buf,ret);
                exit(0);
        }
        if(ret>0)
        {
                int status;
                close(fd[0]);
                char* p="hello!my son\n";
                write(fd[1],p,strlen(p)+1);
                close(fd[1]);
                wait(&status);
                if(WIFEXITED(status))
                {
                        printf("child exit status:%d\n",WEXITSTATUS(status));
                }
        }
        return 0;
}
              
           

命名管道

匿名管道没有名字,只能用于有亲缘关系的进程通信,为了打破这一局限,Linux中设计了命名管道。命名管道又名FIFO(first in first out),它与匿名管道的不同之处在于,命名管道与系统中的一个路径名关联,以文件的形式存在于文件系统中,由此系统中的不同进程可以通过FIFO的路径访问FIFO文件,实现彼此间的通信。FIFO对应的文件没有数据块,其本质与匿名管道相同,都是由内核管理的一块缓存,对该文件进行读写不会改变文件的大小。

int mkfifo(char * path,mod_t mode);
           

示例

write_fifo.c

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

int main()
{
        int r=mkfifo("fifo",0664);
        if(r==-1)
        {
                perror("fifo");
                exit(1);
        }
        int i=0;
        int fd=open("fifo",O_WRONLY);
        char* p="hello!fifo";
        while(1)
        {
                printf("%d\n",i++);
                write(fd,p,strlen(p));
                sleep(1);
        }
        close(fd);
        return 0;
}

           

read_fifo.c

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
        int fd=open("fifo",O_RDONLY);
        if(fd==-1)
        {
                perror("open");
        }
        char buf[64]={0};
        while(1)
        {
                memset(buf,0,sizeof(buf));
                read(fd,buf,64);
                write(STDOUT_FILENO,buf,strlen(buf));
                sleep(1);
        }
        close(fd);
        return 0;
}

           

消息队列

消息队列的实质是一个存放消息的链表,该链表由内核维护;消息队列可以实现无亲缘关系的进程通信,且独立于通信双方的进程之外。若没有删除内核中的消息队列,即便所有使用消息队列的进程都已经终止,消息队列仍存在于内核中。直到内核重新启动、管理命令被执行或调用系统接口删除消息队列时,消息队列才会真正被销毁。

系统中最大消息队列数与最大消息数都有一定限制,分别由宏MSGMNI、MSGTOL定义;消息队列的每个消息中所含数据块的长度以及队列中所含数据块的总长度也有限制,分别由宏MSGMAX、MSGMNB定义;

使用消息队列实现进程通信的步骤如下:

  1. 创建消息队列。
  2. 发送消息到消息队列。
  3. 从消息队列读取数据。
  4. 删除消息队列。

实现进程间通信需要的四个函数

若调用成功,返回消息队列的标识符,若失败返回-1,errno被设置;

key表示消息队列的键值,若键值为IPC_PRIVATE,将会创建一个之只能被创建消息队列读写的消息队列;键值是消息队列在内存级别的唯一标识,不同进程要互相通信,使用msgget()函数时必须要有相同的键值

msgflg用于设置消息队列的创建方式和权限,通常由一个9位的权限位和与以下值进行或操作而得:

msgflg=0664|IPC_CREAT时,内核中不存在消息队列,该函数会创建一个消息队列,如果已存在消息队列,则获取该消息队列;

msgflg=0644|IPC_CREAT|IPC_EXCL时,消息队列不存在,则创建一个消息队列,若存在,则调用失败,返回-1并设置errno为EEXIST;

若该函数调用成功,则返回消息队列得标识符,否则返回-1并设置errno;

msgsnd()函数发送得消息受到两项约束:一是消息长度必须小于系统规定上限;二是消息必须以一个长整型变量开始,因为需要利用此变量先确定消息类型。Linux系统定义了一个模板数据结构:

struct msgbuf
{
	long int msgtype;
	anytype data;
}
           

msgsnd()中得参数msqid表示消息队列表示符,与msgsnd()函数返回值相同;msqp表示指向消息缓冲区得指针;msgsz表示消息中数据的长度,这个长度不包括长整型成员变量的长度;msgflg为标志位,可以设置为0或IPC_NOWAIT。若消息队列已满或系统中的消息数量达到上限,函数立即返回-1;当msgflg设置为0时,调用函数的进程会被挂起,直到消息写入消息队列为止;

若函数调用成功返回消息对列标识符,否则返回-1并设置errno;

msqid表示消息队列的id,通常由msgget()函数返回;参数msgp为指向所读消息的结构体指针;msgsz表示消息的长度,这个长度不包含整型成员变量的长度;参数msgtype表示消息队列中读取的消息类型,其取值以及各值代表的含义分别如下:

  • msgtype=0,表示获取队列中的第一个可用消息;
  • msgtype>0,表示获取队列中与该值类型相同的第一个消息;
  • msgtype<0,表示获取队列中消息类型小于或等于其绝对值的第一个消息;

    最后一个参数msgflg依然为标志位。msgflg设置为0时,进程将阻塞等待消息的读取;msgflg设置为IPC_NOWAIT时,进程未读到指定消息时将立刻返回-1。

若该函数调用成功返回消息队列标识符,若调用失败返回-1并设置errno;

msgctl()函数功能的选择与参数有关,其中参数msqid表示消息队列的id,通常由msgget()返回;参数cmd表示消息队列的处理命令,通常由以下几种取值:

  • IPC_RMID该取值表示msgctl()函数将从系统内核中删除指定的消息队列;
  • IPC_SET该取值若表示进程有权限,就将内核管理的消息队列的当前属性值设置为参数buf各成员的值;
  • IPC_STAT该取值表示将内核所管理的消息队列的当前属性值复制给参数buf;

参数buf是一个缓冲区,用于传递属性值给消息队列或获取消息队列的属性,其功能视参数cmd而定。数据类型为struct msqid-ds为一个结构体

struct msqid_ds
{
	struct ipc_perm msg_perm;//所有者和权限标识
	time_t msg_stime;//最后一次发送消息的时间
	time_t msg_rtime;//最后一次接收消息的时间
	time_t msg_ctime;//最后改变的时间
	unsigned ling _msg_cbytes;//队列中当前数据字节数
	msgqnum_t msg_qnum;//队列中当前消息数
	msglen_t msg_qbytes;//队列允许的最大字节数
	pid_t msg_lspid;//最后发送消息的进程的pid
	pid_t msg_lrpid;//最后接收消息的进程的pid
};
           

例子

msg_queue_snd.c

#include<stdio.h>
#include<stdlib.h>
#include<sys/msg.h>
#include<string.h>
#include<sys/types.h>
#include<sys/ipc.h>
#define MAX_LINE 128
struct msgbuf
{
        long int msgtype;
        char mtext[MAX_LINE];
};
int main()
{
        int index=1;
        struct msgbuf data;
        data.msgtype=1;
        char buf[BUFSIZ];
        int msgid=msgget((key_t)4317,0664|IPC_CREAT);
        if(msgid==-1)
        {
                perror("msgget");
                exit(EXIT_FAILURE);
        }
        while(index)
        {
                fgets(buf,BUFSIZ,stdin);
                strcpy(data.mtext,buf);
                if(msgsnd(msgid,(void*)&data,sizeof(data.mtext),0)==-1)
                {
                        perror("msgsnd");
                        exit(EXIT_FAILURE);
                }
        }
        return 0;
}

           

msg_queue_rcv.c

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/msg.h>
#include<string.h>
#include<sys/types.h>
#include<sys/ipc.h>
#define MAX_LINE 128
struct msgbuf
{
        long int msgtype;
        char mtext[MAX_LINE];
};
int main()
{
        int index=1;
        struct msgbuf data;
        data.msgtype=0;
        char buf[BUFSIZ];
        int msgid=msgget((key_t)4317,0664|IPC_CREAT);
        if(msgid==-1)
        {
                perror("msgget");
                exit(EXIT_FAILURE);
        }
        while(index<5)
        {
                if(msgrcv(msgid,(void*)&data,sizeof(data.mtext),data.msgtype,MSG_NOERROR)==-1)
                {
                        perror("msgsnd");
                        exit(EXIT_FAILURE);
                }
                printf("%ld",data.msgtype);
                printf("%s",data.mtext);
                index++;
        }
        if(msgctl(msgid,IPC_RMID,0)==-1)
        {
                perror("msgctl");
                exit(EXIT_FAILURE);
        }
        return 0;
}

           

信号量

Linux中多个进程间可能因为进程合作或资源共享而产生制约关系。这种制约关系分为直接相互制约关系和间接相互制约关系;

  • 直接相互制约关系:直接相互制约关系有同步关系,比如管道读端和写端的进程;
  • 间接相互制约关系:简介相互制约的程序具有互斥关系,因为资源共享而导致的制约关系叫间接相互制约关系;

信号量是专门用于解决进程同步与互斥问题的一种通信机制;

不同的进程通过同一个信号量键值获取信号量进行通信,实现进程间对资源的互斥访问。使用信号量通信时,需要以下步骤:

  1. 创建信号量/信号量集或者获取系统中已有的信号量/信号集;
  2. 初始化信号量。早期的信号量通常被初始化为1,但有些进程一次需要多个同类的临界资源过多个不同类型且不唯一的临界资源,因此可能需要初始化的不是信号量而是一个信号量集;
  3. 信号量的P、V操作根据进程请求修改信号量的数量。执行P操作会使信号量-1,执行V操作会使信号量+1;
  4. 从系统中删除不需要的信号量。系统中信号量的数量是有限制的,最大值由宏SEMMSL设定。

Linux内核提供另外三个系统调用用于实现以上步骤semget()、semctl()、semop;

若该函数调用成功,则返回信号量的标识符,否则返回-1并设置errno。常见的errno值有:

  • EAACES 表示无访问权限;
  • ENOENT表示传入的键值不存在;
  • EINVAL 表示nsems小于0,或信号量数已达上限;
  • EEXIST 当semflg设置指定了ICP_CREAT和IPC_EXCL时,表示该信号量已经存在;

semget()函数中的参数key表示信号量的键值,通常为一个整数;参数nsems表示创建的信号量数目;参数semflg为标志位,权限位可以与IPC_CREAT以及IPC_EXCL发生位或,若该标志位设置为IPC_PRIVATE,表示该信号量为当前进程的私有信号量;

若该函数调用成功,则根据参数cmd返回相应的信息,否则返回-1并设置errno;

semid表示信号量标识符;

semnum表示信号量在信号量集中的编号,该参数在使用信号量集的时候才会使用,通常设置为0,表示区第一个信号;

参数cmd表示对信号量进行的操作;最后一个参数时一个可选参数,依赖于参数cmd,使用该参数时,用户必须在程序中自定义一个如下所示的共用体:

union semnum{
	int val;//cmd为SETVAL时,用于指定信号量值
	struct semid_ds * buf;//cmd为IPC_STAT时或IPC_SET时生效
	unsigned short * array;//cmd为GETALL或SETVAL时生效
	struct seminfo * _buf;//cmd为IPC_INFO时生效
};
           

公用体中的struct semid_ds结构体是由一个内核维护的记录信号量属性信息的结构体,该结构体的定义如下:

struct semid_ds{
	struct ipc_perm sem_perm;//所有者和标识权限
	time_t sem_otime;//最后操作时间
	time_t sem_ctime;//最后更改时间
	unsigned short sem_nsems;//信号集中的信号数量
};

           

cmd的常用设置值为SETVAL和IPC_RMID

  • SETVAL表示semctl()的功能为初始化信号量的值,信号量值通过可选参数传入,在使用信号量前应先对信号量值进行设置;
  • IPC_RMD表示semctl()的功能为从系统中删除指定信号量;信号量的删除应由其所有者或创造者进行,没有被删除的信号量会一直存在于系统中;

调用成功返回0,否则返回-1并设置errno;

semop()函数中参数nsops表示参数sops所指数组中元素的个数;

参数sops为一个struct sembuf类型的数组指针,该数组中每个元素设置了要对信号量集中的某个信号做何种操作,

struct sembuf{
	short sem_num;//信号量在信号量集中的编号
	short sem_op;//信号量操作
	short sem_flag;//标志位
};
           

当结构体成员sem_op设置为-1时,表示P操作,设置为+1时表示V操作。结构体成员sem_flag通常设置为SEM_UNDO,若进程退出前没有删除信号量,则信号量将会由系统自动释放。

示例:

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

static int sem_id;
//设置信号量值
union semu
{
        int val;
        struct semid_ds* buf;
        unsigned short* array;
        struct seminfo* _buf;
};
static int set_semvalue()
{
        union semu sem_union;
        sem_union.val=1;
        if(semctl(sem_id,0,SETVAL,sem_union)==-1)
                return 0;
        return 1;
}

static void del_semvalue()
{
        union semu sem_union;
        if(semctl(sem_id,0,IPC_RMID,sem_union)==-1)
                perror("semctl del");
}
//P操作,获取信号量
static int semaphore_p()
{
        struct sembuf sem_b;
        sem_b.sem_num=0;
        sem_b.sem_op=-1;
        sem_b.sem_flg=SEM_UNDO;
        if(semop(sem_id,&sem_b,1)==-1)
        {
                perror("sem_p");
                return 1;
        }
        return 0;
}
//V操作,释放信号量
static int semaphore_v()
{
        struct sembuf sem_b;
        sem_b.sem_num=0;
        sem_b.sem_op=1;
        sem_b.sem_flg=SEM_UNDO;
        if(semop(sem_id,&sem_b,1)==-1)
        {
                perror("sem_v");
                return 1;
        }
        return 0;
}
int main()
{
        int i;
        pid_t pid;
        char ch='C';
        //创建信号量
        sem_id=semget((key_t)1000,1,0644|IPC_CREAT);
        if(sem_id==-1)
        {
                perror("semget");
                exit(EXIT_FAILURE);
        }

        //设置信号量值
        if(!set_semvalue()==-1)
        {
                perror("semctl");
                exit(EXIT_FAILURE);
        }
        pid=fork();//创建子进程
        if(pid==-1)
        {
                del_semvalue();//创建子进程失败,删除信号量
                exit(EXIT_FAILURE);
        }
        else if(pid==0)
                ch='Z';
        else
        {
                ch='C';
        }
        srand((unsigned int)getpid());
        for(i=0;i<8;i++)
        {
                semaphore_p();
                printf("%c",ch);
                fflush(stdout);//将字母打印到屏幕
                sleep(rand()%4);
                printf("%c",ch);
                fflush(stdout);
                sleep(1);
                semaphore_v();//释放信号量
        }

        if(pid>0)
        {
                wait(NULL);
                del_semvalue();
        }
        printf("process %d finished\n",getpid());
        return 0;
}

           

用ftok()获取键值

此函数的参数pathname表示路径名,一般为当前路径。proj_id是一个整型数据,由用户指定,一般为0;此函数会通过pathname获取目录的inode然后将十进制的inode和proj_id转换为十六进制,然后再将两个数连接在一起,生成一个key_t类型的值然后返回;

共享内存

共享内存:不同进程通过不同的虚拟内存映射到同一块物理内存;

在进程的写操作完成之前,不应有进程从共享内存中读取数据,共享内存本身不限制进程对内存的读写次序,但程序员应当自觉遵守读写规则,一般情况下共享内存应与信号量一起使用,由信号量帮他实现读写操作同步;

shmget()函数用于创建共享内存;

shmat()函数用于绑定共享内存;

shmdt()函数用于解除绑定;

shmctl()可以用来删除共享内存等;

共享内存和信号量、消息队列一样,在使用完后都需要进行释放;

子进程会继承父进程已经绑定的共享内存;

当调用exec函数更改子进程功能以及调用exit()函数时,子进程都会解除与共享内存的映射关系,因此必要时仍应使用shmctl()函数对共享内存进行删除;

示例

#include<stdio.h>
#include<stdlib.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
#define SEGSIZE 4096

typedef struct
{
        char name[8];
        int age;
}Stu;
int main()
{
        int i;
        char name[8];
        int shm_id;
        key_t key;
        Stu* smap;
        //获取关键字
        key=ftok(".",0);
        printf("key=%d\n",key);
        if(key==-1)
        {
                perror("ftok erro");
                exit(EXIT_FAILURE);
        }

        //创建共享内存
        shm_id=shmget(key,SEGSIZE,0644|IPC_CREAT|IPC_EXCL);
        if(shm_id==-1)
        {
                perror("shmget");
                exit(EXIT_FAILURE);
        }

        printf("shm_id=%d\n",shm_id);
        smap=(Stu*)shmat(shm_id,NULL,0);
        memset(name,0x00,sizeof(name));
        strcpy(name,"Jhon");
        name[4]='0';
        //写数据
        for(i=0;i<3;i++)
        {
                name[4]+=1;
                strncpy((smap+i)->name,name,5);
                (smap+i)->age=20+i;
        }
        //解除绑定
        if(shmdt(smap)==-1)
        {
                perror("shmdt");
                exit(EXIT_FAILURE);
        }
        return 0;
}

           
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
typedef struct
{
        char name[8];
        int age;
}Stu;

int main()
{
        struct shmid_ds buf;
        int shm_id,i;
        key_t key;
        Stu* smap;
        //获取键值
        key=ftok(".",0);
        if(key==-1)
        {
                perror("ftok");
                exit(EXIT_FAILURE);
        }
        printf("key=%d\n",key);
        //创建共享内存
        shm_id=shmget(key,0,0);
        printf("shm_id=%d\n",shm_id);
        if(shm_id==-1)
        {
                perror("shmget");
                exit(EXIT_FAILURE);
        }
        //将进程与共享内存绑定
        smap=(Stu*)shmat(shm_id,NULL,0);
        //读数据
        for(i=0;i<3;i++)
        {
                printf("name:%s\n",(smap+i)->name);
                printf("name:%d\n",(smap+i)->age);
        }
        //解除绑定
        if(shmdt(smap)==-1)
        {
                perror("shmdt");
                exit(EXIT_FAILURE);
        }
        //删除共享内存
        shmctl(shm_id,IPC_RMID,&buf);
        return 0;
}

           

继续阅读