IO - 进程间通信

(1) 早期进程间通信:

无名管道(pipe)、有名管道(fifo)、信号(signal)

(2) systemVIPC:

共享内存(shared memory)、消息队列(message queue)、信号灯集(semaphore set)

(3) BSD:

套接字(socket)

一、无名管道

 

1.特点

(1) 只能用于具有亲缘关系的进程之间的通信

(2) 半双工的通信模式,具有固定的读端fd[0]和写端fd[1]。

(3) 管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如 read、write函数。

(4) 管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符 fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。

2.函数接口

int pipe( int fd[2])

功能:创建无名管道

参数:文件描述符 fd[0]:读端     fd[1]:写端

返回值:成功 0

                失败 -1

3.注意事项

(1) 当管道中无数据,读操作会阻塞

        当管道中有数据关闭写端,可以将数据读出。

        当管道中无数据关闭写端,读操作会立即返回。

(2) 管道中写满(管道大小64K)数据写操作会阻塞,一旦用4K空间,写继续

(3) 只有当管道读端存子时,向管道中写入数据才有意义。否则会导致管道破裂。

练习:父子进程实现通信,父进程循环从终端输入数据,子进程循环打印数据,当输入quit结束。

#include <unistd.h>
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <semaphore.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{

    int fd[2] = {0}; // 管道标识
    pid_t pid;       // 进程标识
    char buf[33] = "";
    // 创建管道并判断
    if (pipe(fd) < 0)
    {
        perror("pipe err");
        return -1;
    }

    // 判断子进程是否创建成功
    if ((pid = fork()) < 0)
    {
        perror(" fork error");
        return -1;
    }
    else if (pid == 0) // 子进程
    {
        while (1)
        {
            read(fd[0], buf, 32);  // 通过fd[0]端口读取管道中的数据

            if (strcmp(buf, "quit") == 0)
                exit(0);
            printf("%s \n", buf);
        }
    }
    else // 父进程
    {
        while (1)
        {
            //输入
            scanf("%s", buf);
            // 通过fd[1]写端向管道写入数据
            write(fd[1], buf, 32);

            if (strcmp(buf, "quit") == 0)
                exit(0);
        }
    }

    wait(NULL);

    return 0;
}

二、有名管道

1. 特点

(1) 有名管道可以使互不相关的两个进程互相通信。

(2) 有名管道可以通过路径名来指出,并且在文件系统中可见,但内容存放在内存中。但是读写数据不会存在文件中,而是在管道中。

(3) 进程通过文件IO来操作有名管道

(4) 有名管道遵循先进先出规则

(5) 不支持如lseek() 操作

 

2.函数接口

int mkfifo( constchar *filename, mode_t mode);

功能:创健有名管道

参数:filename:有名管道文件名

                mode:权限

返回值:成功:0

                失败:-1,并设置errno号

注意对错误的处理方式:

如果错误是file exist时,注意加判断,如:if(errno == EEXIST)

注:函数只是在路径下创建管道文件,往管道中写的数据依然写在内核空间。

先创建有名管道,然后用文件IO操作:打开、读写和关闭。

3. 注意事项

(1) 只写方式打开, 阻塞,一直到另一个进程把读打开

(2) 只读方式打开, 阻塞,一直到另一个进程把写打开

(3) 可读可写,如果管道中没有数据,读阻塞

4.练习

练习:创建管道

int main(int argc, char const *argv[])
{
    int fd;
    char buf[32] = "";

    // 创建有名管道 
    if (mkfifo("fifo", 0666) < 0)
    {
        if (errno == EEXIST)
            printf(" file exist \n");
        else
        {
            perror("mkfifo err");
            return -1;
        }
    }

    printf(" mkfifo success \n");

打开读写文件 

// 1. `O_RDONLY`:只写方式打开阻塞。这表示该文件描述符以只读方式被打开,
// 如果尝试写入数据到该管道,会一直阻塞,直到另一个进程使用写方式打开该管道。

// 2. `O_WRONLY`:只读方式打开阻塞。这表示该文件描述符以只写方式被打开,
// 如果尝试从该管道读取数据,会一直阻塞,直到另一个进程使用读方式打开该管道。

// 3. `O_RDWR`:可读可写。
// 如果管道中没有数据可读,读取操作会阻塞,直到有数据可读;
// 如果管道已满,写入操作也会阻塞,直到有足够的空间可写。

int main(int argc, char const *argv[])
{
    int fd;
    char buf[32] = "";

    // 创建有名管道
    if (mkfifo("fifo", 0666) < 0)
    {
        if (errno == EEXIST)
            printf(" file exist \n");
        else
        {
            perror("mkfifo err");
            return -1;
        }
    }

    printf(" mkfifo success \n");

    // 打开管道
    fd = open("fifo", O_RDWR);
    printf("fd:%d \n", fd);

    // 读写文件
    // 向管道中写入 hello
    write(fd, "hello", 5);
    // 读取管道中的 hello
    read(fd, buf, 32);
    printf("%s \n", buf);

    return 0;
}

5. 有名管道和无名管道的区别

无名管道

有名管道

使用场景

只能用于具有亲缘关系的进程之间的通信

不相干的两个进程也可以

特点

半双工的通信模式

具有固定的读端fd[0]和写端fd[1]。

通过文件IO操作

管道是基于文件描述符的通信方式。

在文件系统中会存在管道文件,但是数据放在内核空间

通过文件IO进行操作

遵循先进先出,不支持lseek操作

函数

pipe()

直接read、write

mkfifo()

先打开open,再读写read、write

读写特性

管道中无数据,读阻塞

关闭写端,有数据读出,无数据立即返回

管道写满64k,写阻塞,直到有4k空间才能继续写入

关闭读端,写入会使管道破裂

只写方式打开会阻塞,一直到另一个进程把读方式打开

只读方式打开会苏俄,一直到另一个进程把写方式打开

可读可写,如果管道中无数据读会阻塞

三、信号

1.概念

1)信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式

2)信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。

3)如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

信号的生命周期:

 

2.信号的响应方式

1)忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。

2)捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。

3)执行缺省操作:Linux对每种信号都规定了默认操作

3. 信号种类

SIGINT(2):中断信号,Ctrl-C 产生,用于中断进程

SIGQUIT(3):退出信号, Ctrl-\ 产生,用于退出进程并生成核心转储文件

SIGKILL(9):终止信号,用于强制终止进程。此信号不能被捕获或忽略。

SIGALRM(14):闹钟信号,当由 alarm() 函数设置的定时器超时时产生。

SIGTERM(15):终止信号,用于请求终止进程。此信号可以被捕获或忽略。termination

SIGCHLD(17):子进程状态改变信号,当子进程停止或终止时产生。

SIGCONT(18):继续执行信号,用于恢复先前停止的进程。

SIGSTOP(19):停止执行信号,用于强制停止进程。此信号不能被捕获或忽略。

SIGTSTP(20):键盘停止信号,通常由用户按下 Ctrl-Z 产生,用于请求停止进程。

4.函数接口

(1) 信号发送和挂起

 #include <signal.h>

int kill(pid_t pid, int sig);

功能:给指定进程发送信号

参数:pid:指定进程

           sig:要发送的信号

返回值:成功:0

              失败:-1

int raise( int sig);

功能:向当前进程发送信号

参数: sig:信号

返回值: 成功:0

失败:-1

相当于:kill(getpid(),sig);

int pause(void);

功能:用于将调用进程挂起,直到收到被捕获处理的信号为止。

练习:发送信号

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    kill(getgid(), SIGKILL); // 给指定进程发送信号
    //raise(SIGKILL); // 给当前进程发送信号

    // while(1) ;

    //pause(); // 将进程挂起,作用和死循环类似,但是不占用

    return 0;
}

 (2)定时器

unsigned int alarm( unsigned int seconds)

功能:功能:在进程中设置一个定时器。 当定时器指定的时间到了时,它就向进程发送SIGALARM信号。系统对SIGALARM信号默认处理方式是退出进程。

参数:参数: seconds: 定时时间,单位为秒

返回值:返回值:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。

注意:注意:一个进程只能有一个闹钟时间。如果在调用alarm时已设置过闹钟时间,则之前的闹钟时间被新值所代替。

常用操作:取消定时器alarm(0),返回旧闹钟余下秒数。

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    printf(" %d \n", alarm(10)); // 设置闹钟十秒后响铃
    sleep(2);
    printf("%d \n", alarm(3)); // 重新设置闹钟三秒后响铃,返回上一个闹钟的剩余秒数,以最后一次闹钟为准
    pause();

    return 0;
}

(3)信号处理函数 signal()

typedef void( *sighandler_t)(int);

sighandler_t signal( int signum, sighandler_t handler);

功能:信号处理函数

参数:signum:要处理的信号

        handler:信号处理方式

                SIG_IGN:忽略信号 (忽略 ignore)

                SIG_DFL:执行默认操作 (默认 default)

                handler:捕捉信号 (handler为函数名,可以自定义)

void handler( int sig){}//函数名可以自定义, 参数为要处理的信号

返回值:成功:设置之前的信号处理方式

失败:-1

五、共享内存

1.特点

(1)共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。

(2)为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程

将其映射到自己的私有地址空间。进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。

(3) 由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等

2.步骤

(1) 创建 key值

(2) 创建或打开共享内存

(3) 映射共享内存到用户空间

(4) 撤销映射

(5) 删除共享内存

3.函数接口

key_t ftok( constchar *pathname, int proj_id);

功能:创建出来的具有唯一映射关系的一个key值,帮助操作系统用来标识一块共享内存

参数:Pathname:已经存在的可访问文件的名字

                Proj_id:一个字符(因为只用低8位)

返回值:成功:key值

               失败:-1

// 将文件的索引节点号取出ls -i,前面加上proj_id得到key_t的返回值。如指定文件的索引节点号为65538,换算成16进制为 0x010002取后4位,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。中间的01是系统编号。

int shmget( key_t key, size_t size, int shmflg);

功能:创建或打开共享内存

参数:

        key 键值

        size 共享内存的大小

        shmflg IPC_CREAT|IPC_EXCL|0777

返回值:成功 shmid

              出错 -1

//当IPC_CREAT | IPC_EXCL时, 如果没有该块共享内存,则创建,并返回共享内存ID。若已有该块共享内存,则返回-1。

void *shmat( int shmid, constvoid *shmaddr, int shmflg); //attaches

功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问

参数:

        shmid 共享内存的id号

        shmaddr 一般为NULL,表示由系统自动完成映射

                        如果不为NULL,那么有用户指定

        shmflg:SHM_RDONLY就是对该共享内存只进行读操作

                        0 可读可写

返回值:成功:完成映射后的地址,

              出错:-1(地址)

用返回值判断用法:if((p =(char*)shmat(shmid,NULL,0))==(char*)-1)

int shmdt( constvoid *shmaddr); //detaches

功能:取消映射

参数:要取消的地址

返回值:成功0

                失败的-1

int shmctl( int shmid, int cmd, struct shmid_ds *buf);    //control

功能:(删除共享内存),对共享内存进行各种操作

参数:

        shmid 共享内存的id号

        cmd IPC_STAT 获得shmid属性信息,存放在第三参数

                IPC_SET 设置shmid属性信息,要设置的属性放在第三参数

                IPC_RMID:删除共享内存,此时第三个参数为NULL即可

        buf shmid所指向的共享内存的地址,空间被释放以后地址就赋值为null

返回:成功0

失败-1

用法: shmctl( shmid, IPC_RMID, NULL);

4.命令

ipcs -m: 查看系统中的共享内存

ipcrm -m shmid:删除共享内存

ps: 可能不能直接删除掉还存在进程使用的共享内存。

这时候可以用 ps -ef对进程进行查看,kill 掉多余的进程后,再使用 ipcs查看。

六、信号灯集

线程:全局变量,同步通过信号量。

初始化:sem_init(&sem,0,0);

申请资源:sem_wait(&sem);P操作-1

释放资源: sem_post(&sem); V操作+1

1.特点

信号灯(semaphore),也叫信号量,信号灯集是一个信号灯的集合。它是不同进程间或一个给定进程内部不同线程间同步的机制;

而Posix信号灯指的是单个计数信号灯:无名信号灯、有名信号灯。(咱们学的是无名信号灯)

System V的信号灯是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。

通过信号灯集实现共享内存的同步操作

2.步骤

(1) 创建key值:ftok

(2) 创建或开信号灯集:semget

(3) 初始化信号灯:semctl

(4) PV操作:semop

(5) 删除信号灯集:semctl

3.命令

ipcs -s:查看信号灯集

ipcrm -s semid:删除指定的信号灯集

注意:有时候可能会创建失败,或者semid为0,所以用命令看看,删了重新创建就可以了。

4.函数接口

int semget( key_t key, int nsems, int semflg);

功能:创建/打开信号灯

参数:key:ftok产生的 key值

        nsems:信号灯集中包含的信号灯数目

        semflg:信号灯集的访问权限,通常为 IPC_CREAT | IPC_EXCL | 0666

返回值:成功:信号灯集ID

              失败:-1

int semctl( int semid, int semnum, int cmd…/*union semun arg*/);

功能:信号灯集合的控制(初始化/删除)

参数:semid:信号灯集ID

        semnum: 要操作的集合中的信号灯编号,信号灯编号从0开始

        cmd:

                GETVAL:获取信号灯的值,返回值是获得值

                SETVAL:设置信号灯的值,需要用到第四个参数:共用体

                IPC_RMID:从系统中删除信号灯集合

返回值:成功 0

              失败 -1

用法:

初始化信号灯集:
需要自定义共用体
union semun{
int val;
};

获取信号灯值:函数semctl(semid,0,GETVAL)的返回值

删除信号灯集:semctl(semid,0,IPC_RMID); //任意传一个信号灯编号就可以删除整个信号灯集

int semop( int semid, struct sembuf *opsptr, size_t nops);

功能:对信号灯集合中的信号量进行PV操作

参数:semid:信号灯集ID

        opsptr:操作方式

        nops: 要操作的信号灯的个数 1个

返回值:成功 :0

               失败:-1

struct sembuf{

                short sem_num;// 要操作的信号灯的编号

                short sem_op; // 0 : 等待,直到信号灯的值变成0

                                        // 1 : 释放资源,V操作

                                        // -1 : 申请资源,P操作

                short sem_flg;// 0(阻塞),IPC_NOWAIT, SEM_UNDO

        };

用法:

申请资源操作:

struct sembuf mysembuf;

mysembuf.sem_num = 0;

mysembuf.sem_op = -1;

mysembuf.sem_flg = 0;

semop( semid, &mysembuf, 1);

释放资源V操作:

mysembuf.sem_num=1;

mysembuf.sem_op = 1;

mysembuf.sem_flg = 0;

semop( semid, &mysembuf, 1);

5.信号灯集操作

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/sem.h>

union semun {
    int val;
};

void init(int semid, int num, int val)
{
    union semun sem;   // 定义共用体 sem
    sem.val = val;
    semctl(semid, num, SETVAL, sem); // 对编号为num的信号灯初始化为 sem
}

void pv(int semid, int num, int op)
{
    struct sembuf buf;
    buf.sem_num = num; // 操作编号为0的信号灯集,
    buf.sem_op = op;   // pv操作
    buf.sem_flg = 0;   // 阻塞
    semop(semid, &buf, 1);
}

int main(int argc, char const *argv[])
{
    int semid;
    key_t key;
    //创建key值
    key = ftok("semset.c", 66);
    if (key < 0)
    {
        perror("ftok error");
        return -1;
    }
    printf("key:%#x \n", key);

    //创建或打开信号灯集
    //没有则创建信号灯集,已有则返回-1
    semid = semget(key, 10, IPC_CREAT | IPC_EXCL | 0666);

    if (semid <= 0)
    {
        if (errno == EEXIST) //如果已存在则直接打开信号灯集
        {
            semid = semget(key, 10, 0666); //直接打开信号灯集,返回信号灯集id
        }
        else
        {
            perror("semget err");
            return -1;
        }
    }
    else // 为了第一次创建信号灯集的时候才初始化,
         //如果系统里已有信号灯集就不再初始化了,而是沿用之前的值
    {
        init(semid, 0, 10); // 对编号为0的信号灯初始化为 10
        init(semid, 1, 0);  // 对编号为1的信号灯初始化为 0
    }
    printf("semid:%d \n", semid);

    // 获取信号灯值
    printf("%d \n", semctl(semid, 0, GETVAL));
    printf("%d \n", semctl(semid, 1, GETVAL));

    pv(semid, 0, -1); // 0号信号灯申请资源
    pv(semid, 1, 1);  // 1号信号灯释放资源

    // 获取信号灯值
    printf("%d \n", semctl(semid, 0, GETVAL));
    printf("%d \n", semctl(semid, 1, GETVAL));

    semctl(semid, 0, IPC_RMID); //指定任意一个信号灯都可以,会删除整个信号灯集

    return 0;
}

七、消息队列

1.特点

消息队列是IPC对象(活动在内核级别的一种进程间通信的工具)的一种

一个消息队列由一个标识符 (即队列ID)来标识

消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等

消息队列可以按照类型(自己设一个值作为类型)来发送/接收消息

2.步骤

(1) 产生key值: ftok()

(2) 创建或打开消息队列msgget()

(3) 添加消息:按照类型把消息添加到以及打开的消息队列末尾msgsnd()

(4) 读取消息:可以按照类型把消息从队列中给你读走msgrcv()

(5) 删除消息队列:msgctl()

3.命令

ipcs -q : 查看消息队列

ipcrm -q msgid : 删除指定的消息队列

4.函数接口

 

int msgget( key_t key, int flag);

功能:创建或打开一个消息队列

参数: key值

                flag:创建消息队列的权限 IPC_CREAT | IPC_EXCL | 0666

返回值:成功:msgid

                失败:-1

int msgsnd( int msqid, constvoid *msgp, size_t size, int flag);

功能:添加消息

参数:msqid:消息队列的ID

           msgp:指向消息的指针。常用消息结构msgbuf如下:

                        struct msgbuf{

                                long mtype;       //消息类型

                                char mtext[N]     //消息正文

                                };

        size:发送的消息正文的字节数

        flag:IPC_NOWAIT 消息没有发送完成函数也会立即返回

                0:直到发送完成函数才返回

返回值:成功:0

失败:-1

使用:msgsnd( msgid, &msg, sizeof(msg) -sizeof(long), 0)

注意:消息结构除了第一个成员必须为long类型外,其他成员可以根据应用的需求自行定义。

int msgrcv( int msgid, void * msgp, size_t size, long msgtype, int flag);

功能:读取消息

参数:msgid:消息队列的ID

        msgp:存放读取消息的空间

        size:接受的消息正文的字节数(sizeof(msgp)-sizeof(long))

        msgtype:

                0:接收消息队列中第一个消息。

                大于0:接收消息队列中第一个类型为msgtyp的消息.

                小于0:接收消息队列中类型值不小于msgtyp的绝对值且类型值又        

                                最小的消息。

        flag:

                0:若无消息函数会一直阻塞

                IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG

返回值:成功:接收到的消息的长度

                失败:-1

int msgctl( int msgqid, int cmd, struct msqid_ds *buf );

功能:对消息队列的操作,删除消息队列

参数:msqid:消息队列的队列ID

        cmd:

                IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。

                IPC_SET:设置消息队列的属性。这个值取自buf参数。

                IPC_RMID:从系统中删除消息队列。

        buf:消息队列缓冲区

返回值:成功:0

失败:-1

用法:msgctl(msgid, IPC_RMID,NULL)

5.消息队列操作

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/msg.h>

struct msgbuf
{
    long type; //消息类型
    int num;   // 消息内容
    char ch;
};

int main(int argc, char const *argv[])
{

    int msgid;
    key_t key;

    //创建key值
    key = ftok(".", 66);
    if (key < 0)
    {
        perror("ftok error");
        return -1;
    }
    printf("key:%#x \n", key);

    // 打开或创建消息队列
    msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);

    if (msgid <= 0)
    {
        if (errno == EEXIST) //如果已存在则直接打开
        {
            msgid = msgget(key, 0666); //直接打开,返回消息队列id
        }
        else
        {
            perror("msgget err");
            return -1;
        }
    }
    printf("msgid:%d \n", msgid);

    // 添加消息
    // 定义消息结构体变量 msg
    struct msgbuf msg;

    msg.type = 10;  // 定义类型为 10的消息类型
    msg.num = 1000; // 消息内容 1000
    msg.ch = 'a';   // 消息内容 a
    // 把msg中的消息添加到 id为msgid的队列中
    msgsnd(msgid, &msg, sizeof(msg) - sizeof(long), 0);

    msg.type = 20;
    msg.num = 2000;
    msg.ch = 'b';
    msgsnd(msgid, &msg, sizeof(msg) - sizeof(long), 0);

    // 读取消息
    // 定义消息结构体变量,用来存放读取的消息
    struct msgbuf m;
    msgrcv(msgid, &m, sizeof(m) - sizeof(long), 20, 0);
    printf("%d %c \n", m.num, m.ch);

    // 删除队列
    msgctl(msgid, IPC_RMID, NULL);

    return 0;
}

  • 22
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值