精华内容
下载资源
问答
  • linux下进程间通信

    2018-09-07 09:04:30
    linux下进程间通信 在linux的生产中我们不可能是单个进程完成某项任务,很多时候需要进程间实现交互。其中包括 数据传输:⼀一个进程需要将它的数据发送给另⼀一个进程 资源共享:多个进程之间共享同样的...

    linux下进程间通信

    在linux的生产中我们不可能是单个进程完成某项任务,很多时候需要进程间实现交互。其中包括

    • 数据传输:⼀一个进程需要将它的数据发送给另⼀一个进程
    • 资源共享:多个进程之间共享同样的资源。
    • 通知事件:⼀一个进程需要向另⼀一个或⼀一组进程发送消息,通知它(它们)发⽣生了某种 事件(如进程 终⽌止时要通知⽗父进程)。
    • 进程控制:有些进程希望完全控制另⼀一个进程的执⾏行(如Debug进程),此时控制进程希望能够拦 截另⼀一个进程的所有陷⼊入和异常,并能够及时知道它的状态改变。
      我们一共有八种基本的进程间通信方式,各有优缺点,分别是命名管道匿名管道消息队列共享内存信号套接字内存映射信号量,其中信号单独为一个章节见下一章博客,接下来逐一介绍七种进程间通信方式。

    匿名管道

    管道是最古老的进程间通信方式,它是一种半双工的通信方式,就是只能进行单向传输,管道的本质是内核的一块缓冲区。在linux中一切皆为文件,我们操作管道的方式就是通过文件进行操作,管道会有两个文件操作符fd[0],fd[1],fd[0]是代表读的一端,fd[1]是代表写的一端,我们在用的时候必须两个进程各关闭对应的文件,第一个进程关闭了读的端口第二个就必须关闭写的端口,由于管道是匿名管道所以不是随便两个进程都可以使用匿名管道只有具有亲缘关系的进程可以使用匿名管道进行通信
    int pipe(int pipefd[2]);
    成功返回0,失败返回错误码。pipefd[0]是读端,pipefd[1]是写端。
    管道的生命周期是跟随进程的,当进程结束时关闭文件即可,并且匿名管道自带同步与互斥

    管道的读写规则

    当没有数据可读时

    • O_NONBLOCK disable:read调⽤用阻塞,即进程暂停执⾏行,一直等到有数据来到为止。
    • O_NONBLOCK enable:read调⽤用返回-1,errno值为EAGAIN。

    当管道数据满了的时候

    • O_NONBLOCK disable: write调⽤用阻塞,直到有进程读⾛走数据
    • O_NONBLOCK enable:调⽤用返回-1,errno值为EAGAIN
    • 如果所有管道读端对应的⽂文件描述符被关闭,则write操作会产⽣生信号SIGPIPE,进⽽而可能导致write 进程退出
    • 当要写⼊入的数据量不⼤大于PIPE_BUF时,linux将保证写⼊入的原⼦子性。
    • 当要写⼊入的数据量⼤大于PIPE_BUF时,linux将不再保证写⼊入的原⼦子性
      下面是一个利用匿名管道实现父子进程间通信的小程序
    #include<stdio.h>
    #include<unistd.h>
    #include<errno.h>
    #include<string.h>
    #include<stdlib.h>
    
    int main()
    {
        int fd[2];
        //创建管道
        pid_t pid;
        int n;
        char buf[256]={0};
        //管道创建失败
        if(pipe(fd)!=0)
        {
            perror("fife create error!\n");
            exit(0);
        }
        pid = fork();
        //创建进程失败
        if(pid < 0)
        {
            perror("fork error!\n");
            exit(0);
        }
        //子进程
        else if(pid == 0)
        {
            //子进程关闭读端
            close(fd[0]);
            write(fd[1],"hallo",5);
        }
        //父进程
        else
        {
            //父进程关闭写端
            close(fd[1]);
            read(fd[0],buf,sizeof(buf));
            printf("child say:%s\n",buf);
        }
        return 0;
    }
    
    

    命名管道

    命名管道是文件系统可见,是一个特殊类型的文件
    命名管道可以应用于同意主机上任意进程的进程间通信
    创建:
    1.命令创建:mkfifo pipe_fulename
    2.代码创建:int mkfifo(const char *pathname, mode_t mode);(mode是权限)
    打开特性:因为命名管道需要我们用户自己打开文件,匿名管道创建后直接打开返回描述符
    1.如果只读打开,会阻塞等待这个命名管道被其他进程以写打开
    2.若果只写打开,会阻塞等待这个命名管道被其他进程以读打开
    3.如果以读写打开,则不会堵塞
    读写特性
    1.如果管道没数据,读取操作会阻塞,如果描述符被设置为非阻塞属性,那么这个操作不会被阻塞
    2.如果管道数据满了,写入操作会阻塞
    3.如果管道的写端全部关闭,read读取数据的时候会返回0
    4.如果管道读端全部关闭,那么write写入数据的时候会触发异常,操作系统会发送信号到进程,进程退粗
    5.当写入大小超过PIPE_BUF,则这个操作是非原子操作,可能被打断

    命名管道与匿名管道的不同之处在于命名管道的本质是一个文件,而匿名管道本质是内核的一段缓冲区,命名管道可以用于任何两个进程间进行通信,匿名管道只能用于亲缘间进程通信
    以下是命名管道的代码实现

    #include<stdio.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<errno.h>
    #include<string.h>
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<fcntl.h>
    
    int mian()
    {
        pid_t pid;
        int fd;
        char buf[256]={0};
        if(mkfifo("tp",0664)<0)
        {
            perror("mkfifo error!\n");
            exit(0);
        }
        if(pid<0)
        {
            perror("create pid error!\n");
            exit(0);
        }
        else if(pid == 0)
        {
            //子进程
            fd=open("tp",O_WRONLY);
            write(fd,"hello",5);
            sleep(5);
            close("tp");
        }
        else
        {
            //父进程
            fd=open("tp",O_RDONLY);
            read(fd,buf,sizeof(buf));
            printf("child say:%s\n",buf);
            close("tp");
        }
        return 0;
    }
    
    

    消息队列

    消息队列是内核创建的一个队列,进程可以在这个队列中创建节点,通过这个队列的标识符key,每一个进程都可以找到这个节点,并且与管道不同的是,管道是随着进程的终止结束生命周期的,而消息队列是存在在内核中的所有进程可见的一个队列,所以消息队列的生命周期随内核,管道两个接口一个只能读一个只能写所以管道是一个半双工的通信方式,而消息队列是通过插入节点的方式,所有进程都可以插入节点也都可以读取节点,所以消息队列是一个全双工的通信方式。
    不过事实上这是一种逐渐被淘汰的通信方式,由于我们可以用流管道与套接字更好的替代他所以很不推荐这一种进程间通信方式。
    int msgget(key_t key, int msgflg);
    创建消息队列
    key:内核中消息队列的标识
    msgflg:选项 IPC_CREAT不存在就创建,存在就打开 IPC_EXCL存在就返回
    返回值:操作的句柄,失败就返回-1
    int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
    发送与接收数据
    msqid:msgflg返回的句柄
    msgp:用于接收/发送的数据
    msgsz:用于接收/发送的数据大小
    msgtype:用于接收的数据类型
    msgflg:标志选项
    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    控制/删除消息队列
    msqid:msgflg返回的句柄
    cmd:选项 我们这里用于退出用IPC_RMID
    buf:不关心置NULL
    下面是两个进程间利用消息队列通信的代码示例

    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/ipc.h>
    #include<sys/msg.h>
    #include<unistd.h>
    #include<string.h>
    #include<errno.h>
    
    #define IPC_KEY 0x12345678
    
    #define TYPE_S 1
    #define TYPE_C 2
        
    
    struct msgbuf {
        long mtype;       /* message type, must be > 0 */
        char mtext[1024];    /* message data */
    };
    
    int main()
    {
        int msgid = -1;
        //创建一个消息队列
        msgid = msgget(IPC_KEY,IPC_CREAT | 0664);
        if(msgid<0)
        {
            //创建失败
            perror("msgget error!\n");
            exit(0);
        }
        while(1)
        {
            struct msgbuf buf;
            //发送数据
            memset(&buf,0x00,sizeof(struct msgbuf));
            buf.mtype = TYPE_S;
            scanf("%s",buf.mtext);
            msgsend(msgid,&buf,1024,0);
            //接收数据
            memset(&buf,0x00,sizeof(struct msgbuf));
            msgrcv(msgid,&buf,1024,TYPE_C,0);
            printf("C say : %s\n",buf.mtext);
        }
        return 0;
    }
    

    共享内存

    共享内存是通过一个进程创建一个内存共享区,其他进程可以对这块内存进行读写操作所实现的的进程间通信,共享内存是进程间最快的通信方式,因为不论是管道还是消息队列都经过了内核,多了用户态到内核与内核返回用户态两部操作,需要注意的是共享内存
    我们一般用系统提供的接口shmxxx族函数来实现共享内存删除的时候,如果还有进程与共享内存保持映射关系,那么共享内存不会删除,而是等待这个进程解除映射
    系统为我们提供了一套shmxxx的接口来供我们实现控制共享内存
    int shmget(key_t key, size_t size, int shmflg);
    key:共享内存在内存中的标识
    size:要使用内存的大小
    shmflg:权限选 IPC_CREAT IPC_EXCL
    void *shmat(int shmid, const void *shmaddr, int shmflg);
    映射物理内存
    shmid:shmget返回的操作句柄
    shmaddr:共享内存首地址,置空由系统分配
    shmflg:权限选项 SHM_REONLY只读
    int shmdt(const void *shmaddr);
    解除映射,shamaddr是共享内存首地址
    nt shmctl(int shmid, int cmd, struct shmid_ds *buf);
    shmid:操作句柄
    cmd:权限 IPC_RMID 删除
    buf:用来接收共享内存信息,不关心置空
    下面是共享内存的实现用例

    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<unistd.h>
    #include<errno.h>
    #include<sys/ipc.h>
    #include<sys/shm.h>
    
    #define IPC_KEY 0x12345678
    
    int main()
    {
        int shmid = -1;
        shmid = shmget(IPC_KEY,32,IPC_CREAT|0664);
        if(shmid == -1)
        {
            perror("get shm error!\n");
            exit(0);
        }
        void* start = shmat(shmid,NULL,0);
        if((void*)-1 == start)
        {
            perror("shmat error!\n");
            exit(0);
        }
        while(1)
        {
            printf("please input:");
            memset(start,0x00,32);
            scanf("%s",(char*)start);
            sleep(1);
        }
        shmdt(start);
        shmctl(shmid,IPC_RMID,NULL);
        return 0;
    }
    
    

    信号量

    信号量是用于实现临界资源同步与互斥的一种进程间通信方式,它的本质是一个内核中的计数器,信号量通过对临界资源进行PV操作来实现对临界资源的同步与互斥
    同步:临界资源操作的时序性
    互斥:临界资源同一时间的唯一访问性
    简单来说一个进程或线程在对临界资源进行操作的时候先在信号量这里获取一把锁,且这个锁同一时间只能获取一次(原子操作),只能由锁的持有者释放,当操作完毕将锁释放,也被叫做PV操作,信号量作为进程间通信方式,意味着大家都能访问,信号量实际上也是一个临界资源,当然信号量的这个临界资源操作是不会出问题的,因为信号量的操作是一个原子操作
    信号量分为两种:

    1. 内核信号量,由内核控制使用
    2. 用户态信号量,分为posix与System V两种

    posix中的信号量一般是一个非负整数,常用于线程间同步,而System V中的信号量是一个或多个信号量的集合,常用于进程间同步,相对来说System V信号量更复杂一点。
    ##System V信号量
    System V信号量强调的是一个或多个信号量的集合,对应一个信号量结构体,信号量只是它的一部分,经常用于进程间同步
    int semget(key_t key, int nsems, int semflg);
    创建一个信号量
    key:信号量在内核中的标识
    nsems:创建信号量的个数
    semflg:选项 IPC_CREAT 没有就创建 0664 权限
    int semctl(int semid, int semnum, int cmd, …);
    控制信号量
    semid:操作句柄
    semnum:操作信号量的个数
    cmd:具体的操作 SETALL操作多个信号量 SETVEL操作单个信号量(semnum将被忽略)
    int semop(int semid, struct sembuf sops, unsigned nsops)
    用来创建与访问一个信号量集
    semid:操作句柄
    sops:指向一个结构数值的指针
    nsops:操作的信号量个数
    ###PV操作
    由于PV操作至关重要而且我们这里主要是通过PV操作来控制,所以这里剖析下信号量的PV操作,当我们对临界资源进行操作的时候,内核中提供了信号量来实现进程间通信的同步与互斥,
    每一个进程在对临界资源进行操作的时候先进行P操作,计数器加一,代表这个临界资源已经被获取,当操作完成后计数器减一代表计数器操作完成唤醒其他进程对临界资源进行操作。
    union semun {
    int val; /
    Value for SETVAL */
    struct semid_ds buf; / Buffer for IPC_STAT, IPC_SET */
    unsigned short array; / Array for GETALL, SETALL */
    struct seminfo __buf; / Buffer for IPC_INF (Linux-specific) */
    };
    这个是设置信号量的基本信息
    struct sembuf { short sem_num; short sem_op; short sem_flg; };
    这个是设置信号量进行的操作设置
    num是操作信号量的个数,sem_op是要对信号量进行的操作,-1与+1
    sem_flg的两个取值是IPC_NOWAIT或SEM_UNDO
    在实现中我们先设置信号量的基本信息,在P与V的操作函数里利用第二个结构体设置信号量进行的操作,以下是代码实现

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>
    
    #define IPC_KEY 0x12345678
    union semun {
        int              val;    /* Value for SETVAL */
        struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
        unsigned short  *array;  /* Array for GETALL, SETALL */
        struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                    (Linux-specific) */
    };
    
    void sem_P(int id)
    {
        struct sembuf buf;
        buf.sem_num = 0;
        buf.sem_op = -1;
        buf.sem_flg = SEM_UNDO;
    
        semop(id, &buf, 1);
    }
    
    void sem_V(int id)
    {
        struct sembuf buf;
        buf.sem_num = 0;
        buf.sem_op = 1;
        buf.sem_flg = SEM_UNDO;
    
        semop(id, &buf, 1);
    }
    int main()
    {
        int pid = -1;
        int semid = semget(IPC_KEY, 1, IPC_CREAT | 0664);
        if (semid < 0) {
            perror("semget error");
            return -1;
        }
        union semun val;
        val.val = 1;
        semctl(semid, 0, SETVAL, val);
        pid = fork();
        if (pid < 0) {
            perror("fork error");
            exit(-1);
        }else if (pid == 0) {
            sleep(1);
            while(1) {
                sem_P(semid);
                //对于一元信号量来说,当这个进程获取的信号量之后,那么
                //另一个进程将获取不到信号量,会等待,也就是说,在释放
                //信号量之前,我的临界操作不会被打断
                printf("A");
                fflush(stdout);
                usleep(1000);
                printf("A ");
                fflush(stdout);
                //释放信号量
                sem_V(semid);
            }
        }else {
            //打印B
            while(1) {
                sem_P(semid);
                printf("B");
                fflush(stdout);
                usleep(1000);
                printf("B ");
                fflush(stdout);
                sem_V(semid);
            }
        }
        return 0;
    }
    

    posix信号量留作线程阶段在做讲解,
    以上是常用的几种进程间通信方式。

    展开全文
  • Linux下进程间通信

    千次阅读 2014-01-06 15:23:34
    Linux下进程间通信概述 Linux下的进程通信基本上是从UNIX平台上的进程通信继承来的。而对UNIX发展做出最大贡献的俩大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程的通信方面的侧重点...

    Linux下进程间通信概述

    Linux下的进程通信基本上是从UNIX平台上的进程通信继承来的。而对UNIX发展做出最大贡献的俩大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程的通信方面的侧重点有所不同。前者是对UNIX早期的进程间通信手段进行了系统的改进和扩充,形成了“System V IPC”,其通信主要局限在单个计算机内;后者跳出了该限制,形成了基于套接字(Socket)的进程间通信机制。而Linux则把两者的优势都继承下来。

    LINUX 进程间通信方式:

    传统: /*  通用级 */
    有名管道:mkfifo write read close
    无名管道:pipe write read close
    信号: signal kill raise pause alarm 

    POSIX: /*  新的、轻量级 */
    信号量: man sem_overview
    有名:sem_open sem_close sem_unlink
    无名:sem_init sem_destroy
    sem_post sem_wait sem_getvalue
    共享内存: man shm_overview
    shm_open shm_unlink mmap munmap
    消息队列: man mq_overview
    mq_open mq_close mq_unlink mq_send mq_receive

    system V: /* 古老的、重量级 */
    信号量集:
    semget semctl semop
    共享内存:
    shmget shmctl shmat shmop
    消息队列:
    msgget msgop msgctl msgsnd msgrcv

    BSD:
    Socket通信
    连接:
    SERVER: socket bind listen accept recv send 
    CLIENT: socket bind connect send recv
    无连接:
    SERVER: socket bind recvfrom sendto
    CLIENT: socket bind sendto recvfrom 

    Linux中使用的进程间通信(IPC)方式

    • 管道(Pipe)及有名管道(命名管道)(Named Pipe):管道可以用于具有亲缘关系的进程进行通信。有名管道除了具有管道的所有功能外,还允许无情缘关系的进程间通信。
    • 信号(Signal):信号实在软件层次上的对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理机收到一个中断请求效果上可以说是一样的。信号也是唯一一种进程间异步通信的方式,双方在通信前不用先做好准备。
    • 消息队列(Message Queue):消息队列是消息的链接表,包括POSIX消息队列和System V消息队列,它克服了前两种消息量有限的缺点,并按照权限进行操作消息队列。
    • 共享内存(Shared Memory):共享内存是最高效的进程间通信方式。它使得多个进程可以使用同一块内存空间。但这种通信方式需要依靠某种同步机制,如互斥锁、信号量来保证安全性。
    • 信号量(Semaphore):主要用于进程(线程)间的同步和互斥通信。
    • 套接字(Socket):这是一种应用范围更广的进程间通信方式。它不仅可以在本地进程间通信,还可以用于不同主机内进程通信。

    管道通信

    无名管道
    它只能用于有亲缘关系(父子,兄弟等)的进程间的通信。
    它是一个半双工的通信模式,具有固定的读端和写端。
    管道可以看成特殊的文件,对它的操作可以使用read和write,它不属于任何文件系统,存在于内存中。
    有名管道(命名管道)
    它可以实现不相关的两个进程间彼此通信。
    命令管道FIFO严格按照先进先出的规则。不支持lseek操作。
    命名管道在文件系统中是可见的,使用mkfifo可以创建该类型文件

    常用函数

    pipe,read,write,mkfifo

    注意事项

    int pipe(int fd[2]),下标为0的为读端,下标为1的为写端

    只有在读端存在的时候写入才有意义,否则,向管道写入数据的进程将收到SIGPIPE信号,进程被杀死。

    信号

    信号是在软件层次上对中断机制的一种模拟,信号是异步的,一个进程不必通过任何操作来等待信号到来,即进程处于未执行的状态,内核会保存其信号,待进程执行时在传给它。最常用的发送信号的系统函数有kill(),raise(),alarm(),setitimer()和sigqueue()等。
    进程可以通过三种方式响应一个信号
    忽略信号
    忽略信号即对信号不做任何处理,其中SIGKILL和SIGSTOP不能被忽略
    执行默认操作
    Linux对每种信号都有默认的处理,使用该方式进行处理信号。
    捕捉信号
    使用signal()函数注册信号处理函数,在信号来时执行预定的处理函数。
    常用函数
    发送信号的函数:kill(),raise()
    捕捉信号的函数:alarm(),pause()
    注册信号处理的函数:signal(),sigaction()

    消息队列

    消息队列就是一些消息的列表,用户可以在消息队列中添加小写和读取消息等。从这点上看,消息队列具有FIFO的特性,但是它可实现消息的随机查询,比FIFO具有更大的优势,同时这些消息存在于内核中,有队列ID来标识。使用ipcs -q查看当前系统的消息队列状态。

    常用函数

    ftok() 获取key

    msgget() 获取消息队列id

    msgsnd() 向消息队列发送消息

    msgrcv() 从消息队列接收消息

    msgctl() msg通用控制函数

    共享内存

    为了在多个进程间交换信息,内核专门流出了一块内存区,这块内存可以由需要访问的进程映射到自己的私有地址空间,从而进程可以直接读写这一段内存,不需要复制数据。因此共享内存是最为高效的进程间通信方式。使用ipcs -m查看当前系统中共享内存使用状态。
    常用函数
    ftok()  获得key
    shmget() 获得共享内存id
    shmat() 映射共享内存
    shmdt() 取消映射共享内存
    shmctl() 共享内存控制函数

    信号量

    背景:
    在多任务的操作系统下,进程间可能存在一定的制约关系。例如间接制约和直接制约。
    互斥关系
    间接制约指进程间相互争夺共享资源的关系,例如进程争夺CPU时间片、I/O设备。
    我们把进程间争夺共享资源的关系称为互斥关系。
    同步关系
    直接制约指进程间相互合作的关系,即需要按条件有固定顺序的访问某资源。例如读者与写者问题,进程A的输出结果,进程B需要用到,所以进程B必须先等进程A完成。
    我们把进程间有固定顺序的操作某些资源的合作关系称为同步关系。
    对于同步与互斥的关系我们可以理解为:
    同步关系包含互斥关系,互斥关系是一种特殊的同步关系。
    同步与互斥的根本原因在于资源不足,共享资源。这些共享的资源被称为临界资源,这些操作临界资源的代码称为临界区。

    信号量:
    信号量是用来解决进程间同步与互斥问题的一种进程间通信机制,包括一个称为信号量的变量和在该信号量下等待的资源的进程等待队列,以及信号量进行的两个原子操作(PV操作),其中信号量对应某一种资源,取一个非负整数。信号量的值指当前可用该资源的数量,若为0表示该资源当前没有可用资源。
    多个信号量又被称为信号灯集。
    PV原子操作的具体定义如下
    P操作
    如果有可用资源(信号量值>0),则占用一个资源(给当前信号量值减一,进入临界区代码);如果当前没有可用资源(信号量值=0),则进程被阻塞直到系统将资源分配给该进程(进入等待队列,直到资源轮到该进程使用)。
    V操作
    如果在该信号的等待队列中有进程在等待资源,则唤醒一个阻塞进程;如果没有进程等待该资源,则释放一个资源(给当前信号量值加一)。
    常用函数
    ftok() 获取key
    semget() 获取信号灯集ID
    semop() 对信号灯集操作
    semctl() 信号灯集的控制函数
    sem_wait() 对信号量进行P操作
    sem_post() 对信号量进行V操作
    使用ipcs -s查看当前系统信号量使用情况。

    展开全文
  • Linux 下进程间通信实例之一——共享内存 使用了信号量用于进程间的同步
  • Linux下进程间通信方式及实例Linux下进程间通信方式及实例IPC概念管道管道函数小例子读管道写管道管道的优劣FIFO创建管道例mmap映射mmap函数例mmap九问进程通信例父子进程通信无血缘关系进程通信匿名映射信号信号的...

    Linux下进程间通信方式及实例

    IPC概念

    IPC:InterProcess Communication 进程间通信,通过内核提供的缓冲区进行数据交换的机制。

    IPC通信方式:

    • pipe 管道 – 简单
    • FIFO 有名管道
    • mmap 文件映射共享IO – 速度最快
    • 本地socket 最稳定
    • 信号 携带信息量最小
    • 共享内存
    • 消息队列

    管道

    管道:半双工通信

    管道函数

    int pipe(int pipefd[2])

    • pipefd读写文件描述符,0-代表读,1-代表写
    • 返回值:失败返回-1,成功返回0
    #include <stdio.h>
    #include <unistd.h>
    
    int main(){
        int fd[2];
        pipe(fd);
        pid_t pid = fork();
        
        if (pid == 0){
            write(fd[1], "hello", 5);
        }else if (pid > 0){
            char buf[12];
            int ret = read(fd[0], buf, sizeof(buf));
            if (ret > 0){
                write(STDOUT_FILENO, buf, ret);
            }
        }
    }
    

    小例子

    父子进程实现pipe通信,实现ps aux|grep bash功能

    #include <stdio.h>
    #include <unistd.h>
    
    int main(){
        int fd[2];
        pipe(fd);
        
        pid_t pid = ford();
        if (pid == 0){
            //son -> ps
            //关闭读端
            close(fd[0]);
            //重定向
            dup2(fd[1], STDOUT_FILENO);//标准输出重定向到管道写端
            //execlp
            execlp("ps", "ps", "aux", NULL);
        }else if (pid > 0){
            //关闭写端
            close(fd[1]);
            //重定向:标准输入->管道读端
            dup2(fd[0], STDIN_FILENO);
            execlp("grep", "grep", "bash", NULL);
        }
        return 0;
    }
    

    读管道

    • 写端全部关闭 – read读到0,相当于读到文件末尾
    • 写端没有全部关闭
      • 有数据 – read读到数据
      • 没有数据 – read阻塞 fcntl函数可以更改非阻塞

    写管道

    • 读端全部关闭 – 产生一个信号 SIGPIPE ,程序异常终止
    • 读端没有全部关闭
      • 管道已满 – write阻塞
      • 管道未满 – write正常写入

    管道的优劣

    优点:简单,相比信号,套接字实现进程间通信,简单很多

    缺点:1、只能单向通信,双向通信需要建立两个管道。2、只能用于父子、兄弟进程(有共同祖先)间通信。该问题后来使用FIFO有名管道解决。

    FIFO

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

    FIFO是unix基础文件类型中的一种。但,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。备进程可以打开这个文件进行 read/write,实际上是在读写内核通道,这样就实现了进程间通信。

    创建管道

    • 使用命令:mkfifo myfifo
    • 使用函数:int mkfifo(const char *pathname, mode_t mode); 成功:0;失败:-1

    内核会针对fifo文件开辟一个缓冲区,操作FIFO文件,可以操作缓冲区,实现进程通信。

    一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件IO函数都可以用于FIFO。如:closereadwriteunlink

    写端进程:

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <string.h>
    
    int main(int argc, char * argv[]){
        
        if (argc != 2){
            printf("./a.out fifoname\n");
            return -1;
        }
        //若当前目录有一个myfifo文件
        //打开FIFO文件
        int fd = open(argv[1], O_WRONLY);
        //写
        char buf[256];
        int num = 1;
        while(1){
            memset(buf, 0x00, sizeof(buf));
            sprintf(buf, "xiaoming%04d", num++);
            write(fd, buf, strlen(buf));
            sleep(1);
    	}
        //关闭
        close(fd);
        return 0;
    }
    

    读端进程:

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <string.h>
    
    int main(itn argc, char* argv[]){
        if (argc != 2){
            printf("./a.out fifoname\n");
            return -1;
        }
        int fd = open(argv[1], O_RDONLY);
        char buf[256];
        int ret;
        while(1){
            memset(buf, 0x00, sizeof(buf));
            ret = read(fd, buf, sizeof(buf));
            if (ret > 0) 
                printf("read:%s\n", buf);
    	}
        close(fd);
        return 0;
    }
    

    注意事项:打开FIFO文件的时候,read端会阻塞等待write端打开open,write端同理,也会阻塞等待另外一端打开。

    mmap映射

    mmap函数

    创建映射区

    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

    • addr : 映射地址,可以传NULL
    • length:映射区的长度
    • prot:
      • PROT_EXEC pages may be executed.
      • PROT_READ pages may be read.
      • PROT_WRITE pages may be written.
      • PROT_NONE pages may not be accessed.
    • flags:
      • MAP_SHARED 映射区是共享的,对内存的修改会影响到源文件
      • MAP_PRIVATE 映射区是私有的
    • fd:文件描述符,open打开一个文件
    • offset:偏移量
    • 返回值
      • 成功:返回可用内存首地址
      • 失败:返回MAP_FAILED

    释放映射区

    int munmap(void *addr, size_t length);

    • addr:传mmap的返回值
    • length:mmap创建长度
    • 返回值:
      • 成功:0
      • 失败:-1

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <sys/mman.h>
    #include <string.h>
    
    int main(){
        int fd = open("mem.txt", O_RDWR);
        //创建映射区
        char *mem = mmap(NULL, 8, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
        if (mem == MAP_FAILED){
            perror("mmap err");
            return -1;
        }
        //拷贝数据
        strcpy(mem, "hello");
        //释放mmap
        munmap(mem,8);
        close(fd);
    	return 0;
    }
    

    mmap九问

    1. 如果更改mem变量的地址,释放的时候 munmap,传入mem还能成功吗?

      不能

    2. 如果对mem越界操作会怎么样?

      文件的大小对映射区操作有影响,尽量避免

    3. 如果文件偏移量随便填个数会怎么样?

      offset必须是4k的整数倍

    4. 如果文件描述符先关闭,对mmap映射有没有影响?

      没有影响

    5. open的时候,可以新创建一个文件来创建映射区吗?

      不可以用大小为0的文件,文件大小为0会引起bus error

    6. open文件选择 O_WRONLY,可以吗?

      不可以:Permission Denied,因为把文件映射到缓冲区会进行一次读操作,若没有读操作的权限会报错。

    7. 当选择 MAP_SHARED的时候,open文件选择 O_RDONLY,pot可以选择PROT_READ|PROT_ WRITE吗?

      不可以,同样会报Permission Denied,SHARED的时候,映射区的权限 <= open文件的权限

    8. mmap什么情况下会报错?

      很多情况

    9. 如果不判断返回值会怎么样?

    进程通信例

    父子进程通信

    子进程

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <sys/mman.h>
    #include <string.h>
    #include <sys/wait.h>
    
    int main(){
        int fd = open("mem.txt", O_RDWR);
        //创建映射区
        int *mem = mmap(NULL, 4, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
        if (mem == MAP_FAILED){
            perror("mmap err");
            return -1;
        }
        //fork子进程
    	pid_t pid = fork();
        //父进程和子进程交替修改数据
        if (pid == 0){
            //son
            *mem = 100;
            printf("child, *mem = %d\n", *mem);
            sleep(3);
            printf("child, *mem = %d\n", *mem);
        }else if (pid > 0){
            //parent
            sleep(1);
            printf("parent, *mem = %d\n", *mem);
            *mem = 1001;
            printf("parent, *mem = %d\n", *mem);
            wait(NULL);
        }
    	munmap(mem,4);
        close(fd);
    	return 0;
    }
    

    无血缘关系进程通信

    进程1:修改映射区

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <sys/mman.h>
    #include <string.h>
    #include <sys/wait.h>
    
    typedef struct _Student{
        int sid;
        char sname[20];
    }Student;
    
    int main(int argc, char *argv[]){
        if (argc != 2){
            printf("./a.out fifoname\n");
            return -1;
        }
        int fd = open(argv[1], O_RDWR|O_CREAT|O_TRUNC, 0666);
        int length = sizeof(Student);
        ftruncate(fd, length);
        //创建映射区
        Student *stu = mmap(NULL, length, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
        if (mem == MAP_FAILED){
            perror("mmap err");
            return -1;
        }
        int num = 1;
        while (1){
            stu->sid = num;
            sprintf(stu->sname, "xiaaoming-%03d", num++);
            sleep(1);	//每隔一秒修改映射区内容
        }
    	munmap(mem,length);
        close(fd);
    	return 0;
    }
    

    进程2:读取数据

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <sys/mman.h>
    #include <string.h>
    #include <sys/wait.h>
    
    typedef struct _Student{
        int sid;
        char sname[20];
    }Student;
    
    int main(int argc, char *argv[]){
        if (argc != 2){
            printf("./a.out fifoname\n");
            return -1;
        }
        int fd = open(argv[1], O_RDWR);
        int length = sizeof(Student);
        //创建映射区
        Student *stu = mmap(NULL, length, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
        if (mem == MAP_FAILED){
            perror("mmap err");
            return -1;
        }
        while (1){
            printf("sid = %d, sname = %s\n", stu->sid, stu->sname);
            sleep(1);
        }
    	munmap(mem,length);
        close(fd);
    	return 0;
    }
    

    匿名映射

    通过使用我们发现,使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再 unlink、cose掉,比较麻烦。可以直接使用匿名映射来代替。

    其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数fags来指定。

    使用 MAP_ANONYMOUS(或 MAP_ANON),如:

    int *p=mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);

    "4"随意举例,该位置表大小,可依实际需要填写

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <sys/mman.h>
    #include <string.h>
    #include <sys/wait.h>
    
    int main(){
        //创建映射区
        int *mem = mmap(NULL, 4, PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON, -1, 0);
        if (mem == MAP_FAILED){
            perror("mmap err");
            return -1;
        }
        //fork子进程
    	pid_t pid = fork();
        //父进程和子进程交替修改数据
        if (pid == 0){
            //son
            *mem = 100;
            printf("child, *mem = %d\n", *mem);
            sleep(3);
            printf("child, *mem = %d\n", *mem);
        }else if (pid > 0){
            //parent
            sleep(1);
            printf("parent, *mem = %d\n", *mem);
            *mem = 1001;
            printf("parent, *mem = %d\n", *mem);
            wait(NULL);
        }
    	munmap(mem,4);
        close(fd);
    	return 0;
    }
    

    需注意的是, MAP ANONYMOUS和 MAP ANON这两个宏是Lnux操作系统特有的宏

    这两个宏在有些Unix系统没有,可以用/dev/zero做映射

    信号

    信号的概念

    信号在我们的生活中随处可见,如:古代战争中摔杯为号;现代战争中的信号弹;体育比赛中使用的信号枪…他们都有共性:1.简单 2.不能携带大量信息 3.满足某个特设条件才发送

    信号是信息的载体, Linux/UNⅨ环境下,古老、经典的通信方式,现下依然是主要的通信手段。

    Unix早期版丰就提供了信号机制,但不可靠,信号可能失 Berkeley和A&T都对信号模型做了更改,增加了可靠信号机制。但彼此不兼容。pOS1对可靠信号例程进行了标准化。

    信号的机制

    A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断似——异步模式

    但信号是软件层面上实现的中断,早期常被称为“软中断”。

    信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个廷迟时间非常短,不易察觉

    每个进程收到的所有信号,都是由内核负责发送的,内核处理

    共享内存

    创建共享内存

    用到的函数shmget, shmat, shmdt

    函数名 功能描述
    shmget 创建共享内存,返回pic key(共享内存id)
    shmat 第一次创建完共享内存时,它还不能被任何进程访问,shmat()函数的作用就是用来启动对指定id的共享内存的访问,并把共享内存连接到当前进程的地址空间
    shmdt 该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。

    函数原型如下:

    int shmget(key_t key, size_t size, int flag);
    key: 标识符的规则
    size:共享存储段的字节数
    flag:读写的权限
    返回值:成功返回共享存储的id,失败返回-1
    //
    void *shmat(int shmid, const void *addr, int flag);
    shmid:共享存储的id
    addr:一般为0,表示连接到由内核选择的第一个可用地址上,否则,如果flag没有指定SHM_RND,则连接到addr所指定的地址上,如果flag为SHM_RND,则地址取整
    flag:如前所述,一般为0
    返回值:如果成功,返回共享存储段地址,出错返回-1
    //
    int shmdt(void *addr);
    addr:共享存储段的地址,以前调用shmat时的返回值
    //
    int shmctl(int shmid,int cmd,struct shmid_ds *buf)
    shmid:共享存储段的id
    cmd:一些命令
    
    	IPC_STAT 得到共享内存的状态
            IPC_SET 改变共享内存的状态
            IPC_RMID 删除共享内存 
    
    IPC_RMID 命令实际上不从内核删除一个段,而是仅仅把这个段标记为删除,实际的删除发生在最后一个进程离开这个共享段时。 
    
    #include <stdio.h>
    #include <string.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <stdlib.h>
    
    int main(){
      int shm;
      char* ptr;
    
      shm = shmget(IPC_PRIVATE, 129, IPC_CREAT | 0600);
      if(shm < 0){
        perror("shmget");
        return 1;
      }
    
      ptr = (char*)shmat(shm, NULL , 0);
      if(atoi(ptr) == -1){
        perror("shmat");
        return -1;
      }
      strcpy(ptr, "HELLO");
    
      shmdt(ptr);
    
      return 0;
    }
    

    访问共享内存

    #include <stdio.h>
    #include <string.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <stdlib.h>
    
    int main(int argc, char* argv[]){
      int shm;
      char* ptr;
    
      if(argc != 2){
        return 1;
      }
    
      shm = atoi(argv[1]);
    
      ptr = (char*)shmat(shm, NULL, 0);
      if(atoi(ptr) == -1){
        perror("shmat");
        return 1;
      }
    
      printf("string from shared memory : %s\n", ptr);
    
      shmdt(ptr);
    
      return 0;
    }
    

    删除共享内存

    #include <stdio.h>
    #include <string.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <stdlib.h>
    
    int main(int argc, char* argv[]){
      int shm;
      shmid_ds sds;
    
      if(argc != 2){
        printf("argc is wrong");
        return 1;
      }
    
      shm = atoi(argv[1]);
    
      if(shmctl(shm, IPC_RMID, &sds) != 0){
        perror("shmctl");
        return 1;
      }
    
      return 0;
    }
    
    
    展开全文
  • Linux下进程间通信方式

    1.普通管道,是一种半双工,数据只能从一个方向流向另一个方向,只能用在亲缘进程之间,发送进程将数据送入管道,接受进程从管道中读取数据。使用int pipe(int filedis[2]);函数创建一个普通管道,当一个管道被创建的时候,也创建了两个文件描述符,fieldis[0]是读管道 ,fieldis[1]是写管道。通常是先创建一个管道,然后通过fork函数创建子进程。必须在系统调用fork()前调用pipe(),否则子进程将不会继承文件描述符。否则,会创建两个管道,因为父子进程共享同一段代码段,都会各自调用pipe(),即建立两个管道,出现异常错误。

    int pipe_fd[2];

    pipe(pipe_fd);  创建一个管道

    write(pipe_fd[1],"Hello",5) 向写管道中写入数据。

    read(pipe_fd[0],buf_r,100) 从读管道中读出数据。

    关闭管道,只需要关闭这两个文件描述符就可以了。close(pipe_fd[0]);  close(pipe_fd[1]);

     

    2.命名管道,和普通管道一样也是一种半双工,数据只能从一个方向流向另一个方向,只不过命令管道不仅仅可以使用在亲缘进程之间,不相关的进程也可以使用命名管道进行通信。

    命名管道创建mkfifo("zieckey_fifo",0777);

    写管道打开 fd=open("zieckey_fifo",O_WRONLY); //阻塞写,open阻塞,除非遇到读

    往管道中写入数据 write(fd,s,sizeof(s));

    读管道打开fd=open("zieckey_fifo",O_RDONLY); //阻塞读 open阻塞,除非遇到写

    从管道中读取数据read(fd,buf,sizeof(buf));

    从命名管道的创建和打开,我们可以看出命名管道好像就是一个文件,其实,命名管道就是一种特殊类型的文件,在

    文件系统中以文件名的形式存在。

    打开方式最常见的是阻塞读+阻塞写 非阻塞读+阻塞写

    3.消息队列,消息队列提供了从一个进程向另一个进程发送数据块的方法,消息队列和命名管道很相似。

    消息队列是一个进程向消息队列中发送消息,使用msgsnd函数,另一个进程使用msgrcv函数从消息队列中接受消息。

    消息队列的优势在于,消息队列独立于进程存在,避免了同步命名管道的打开和关闭带来的困难。

    同时消息队列可以避免同步和阻塞问题,不需要进程来提供同步方法。

    另外接受进程可以通过消息类型有选择的接受消息,不像命名管道那样只能默认接受。

    4.共享内存 共享内存就是允许两个不相关的进程访问同一逻辑内存,共享内存是一种在两个正在运行的进程间

    共享和传递数据的一种非常有效的方式。两个不同的进程可以将同一段共享内存连接到各自的字节地址空间,就像是

    这段共享内存是分配给他们的一样,一个进程修改共享内存中的数据,其他进程立刻就会收到影响。

    特别注意一点,共享内存没有任何阻塞同步机制,也就是一个进程正在修改一个数据,并不会阻塞其他进程读取这个

    数据,所以共享内存需要另外提供阻塞同步机制,例如信号量。

    5.信号量 信号量是一个特殊的变量,我们通常通过信号来解决多个进程对同一资源的访问竞争的问题,使在任一时刻只能有一个执行线程访问代码的临界区域,也可以说它是协调进程间的对同一资源的访问权,也就是用于同步进程的。

    例如两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

    6.套接字更为一般的进程间通信机制,可用于不同机器之间的进程间通信


    展开全文

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,111
精华内容 1,244
关键字:

linux下进程间通信

linux 订阅