入门Linux系统编程--进程间通信

一、进程间通信

1、进程间通信概述

  • 进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。

IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

在这里插入图片描述

2、管道通信原理

Linux的管道用于进程间的通信,进程通过PIPE函数,在内核中创建管道,而管道是一个单向队列

PIPE函数注意:

  • 管道是创建在内存中的,进程结束,空间释放,管道就不存在了

  • 管道中的动心,读完了就删除了,是一个单向队列

  • 如果管道中没有东西可读,会读阻塞

  • 当我们写的内容超过管道的空间时,会产生写阻塞

  • 写管道是fd[1],读管道是fd[0]

3、无名管道

无名管道就是一个队列,入队为写,出队为读。无名管道是存在内核中,进程间所有的通信都是在内核间,不是在用户空间。

进程——PIPI函数——内核——管道——读写端(用户端就是文件描述符)

对于无名管道,无法实现非父子进程的通信

  • 管道是通过调用pipe函数创建的

在这里插入图片描述
fd[0]是为读而打开的,fd[1]为写而打开的
在这里插入图片描述

  • 创建一个管道,单一进程,并写入字符串,再读出字符串
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>


int main()
{
        int fd[2];//创建数组,fd[0]是为读而打开的,fd[1]为写而打开的
        char buf[128];
        int pid;
		int ret = pipe(fd);

        if(ret==-1)//创建管道失败则返回-1
        {
                printf("create pipe failed\n");
        }
        pid = fork();
        if(pid<0)
        {
                printf("create child failed\n");
        }
        else if(pid>0){
                sleep(3);
                printf("this is father_pid\n");

                close(fd[0]);//关闭读端
                write(fd[1],"I am handsome",strlen("I am handsome"));//向fd[1]写入内容

                wait(NULL);

        }
        else{

                printf("this is child_pid\n");
                close(fd[1]);//关闭写端
                read(fd[0],buf,128);//从fd[0]读内容到buf中
                printf("read from father_pid:%s\n",buf);//读取父进程写入的数据
                exit(0);//退出子进程
        }

        return 0;
}

运行结果:
在这里插入图片描述

4、创建命名管道

FIFO有时被称为命名管道,是一种特殊文件类型,未命名的管道只能在两个相关的进程之间使用,而且这两个相关的进程还要有一个共同的创建了它们的祖先进程。但是,通过FIFO,不相关的进程也能交换数据。
创建FIFO类似于创建文件
在这里插入图片描述

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include<stdio.h>
//       int mkfifo(const char *pathname, mode_t mode);
int main()
{

        if(mkfifo("./file",0600)==-1 && errno != EEXIST)//如果mkfifo返回值-1,并且错误的原因不是文件存在,则创建管道失败
        {
                printf("mkfifo failed\n");
                perror("why");//打印原因
        }
        else{

                if(errno ==EEXIST){

                        printf("file has existed\n");
                }
                else{
                        printf("mkfifo has succeed\n");
                        }
        }
        return 0;
}

输出结果
如果文件已经存在,则打印file has existed
在这里插入图片描述

5、命名管道的数据通信编程实现

当open一个FIFO时,非阻塞标志(O_NONBLOCK)会产生下列影响:

  • 在一般情况下(没有指定O_NONBLOCK),只读open要阻塞到某个其他进程为写而打开这个FIFO为止。类似地,只写open要阻塞到某个其他进程为读而打开它为止。

  • 如果指定了O_NONBLOCK,则只读open立即返回。但是,如果没有进程为读而打开一个FIFO,那么只写open讲返回-1,并将errno设置成ENXIO。

测试代码:

//FIFO_read.c
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include<stdio.h>
#include<fcntl.h>
#include <unistd.h>

//       int mkfifo(const char *pathname, mode_t mode);
int main()
{
        char buf[30] = {0};//初始化buf数组
        int nread = 0;//初始nread值


        if(mkfifo("./file",0600)==-1 && errno != EEXIST)
        {
                printf("mkfifo failed\n");
                perror("why");
        }


        int fd = open("./file",O_RDONLY);//只读的方式打开
        printf("open successful\n");

        while(1)
        {
                nread = read(fd,buf,sizeof(buf));//把fd的内容读到buf中
                printf("read %d byte from fifo,context:%s\n",nread,buf);
        }

        close(fd);//关闭fd

        return 0;
}
~  
//write_open.c
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include<stdio.h>
#include <unistd.h>
 #include <fcntl.h>
#include<string.h>

int main()
{      
        char *buf = "I will be successful";//写入的内容

        int fd = open("./file",O_WRONLY);//只写的方式打开
        printf("write open ok\n");

        while(1)
        {
                write(fd,buf,strlen(buf));//往fd写内容
                sleep(1);
        }
        close(fd);//关闭fd

        return 0;
}

输出结果:
在这里插入图片描述

6、消息队列的通信原理

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。

特点:

  • 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
  • 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除
  • 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
    在这里插入图片描述

7、消息队列相关api

  • 测试代码:
 #include <sys/msg.h>
 // 创建或打开消息队列:成功返回队列ID,失败返回-1
 int msgget(key_t key, int flag);
 // 添加消息:成功返回0,失败返回-1
 int msgsnd(int msqid, const void *ptr, size_t size, int flag);
 // 读取消息:成功返回消息数据的长度,失败返回-1
 int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制消息队列:成功返回0,失败返回-1
 int msgctl(int msqid, int cmd, struct msqid_ds *buf);

flag位用下表指定:
在这里插入图片描述
在这里插入图片描述

8、消息队列编程收发数据

  • 测试代码:
//Msg_1.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include<stdio.h>
#include<string.h>

//定义一个结构体
struct msgbuf {
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
  };

int main()
{

        struct msgbuf readBuf;
        struct msgbuf sendBuf = {888,"I will be successful"};

        int msgId = msgget(0x1235,IPC_CREAT|0777);
        if(msgId == -1)
        {
                printf("get que failed\n");

        }

        msgrcv(msgId,&readBuf,sizeof(readBuf.mtext),666,0);//接收,如果不是666类型,则一直阻塞

        msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);//发送
        printf("get:%s\n",readBuf.mtext);


        return 0;
}

//Msg_2.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include<stdio.h>
#include<string.h>


struct msgbuf {
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
  };

int main()
{

        struct msgbuf readBuf;
        struct msgbuf sendBuf = {666,"thank you for reaching"};

        int msgId = msgget(0x1235,IPC_CREAT|0777);//返回值是队列ID
        if(msgId == -1)失败返回-1
        {
                printf("get que failed\n");
        }
        msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);
        msgrcv(msgId,&readBuf,sizeof(readBuf.mtext),888,0);//如果不是888类型,则一直阻塞

        printf("get:%s\n",readBuf.mtext);
        return 0;
}
~ 

输出结果:
在这里插入图片描述

9、键值生成及消息队列移除

  • *头文件:
    #include <sys/types.h>
    #include <sys/ipc.h>

  • 函数原型:
    key_t ftok( const char * fname, int id )
    fname就是你指定的文件名(已经存在的文件名),一般使用当前目录,如:
    key_t key;
    key = ftok(“.”, 1); 这样就是将fname设为当前目录。

    id是子序号。虽然是int类型,但是只使用8bits(1-255)。
    在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。
    如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。
    查询文件索引节点号的方法是: ls -i 或者ls -ai(获取当前文件的索引值)
    当删除重建文件后,索引节点号由操作系统根据当时文件系统的使用情况分配,因此与原来不同,所以得到的索引节点号也不同。

  • 测试代码:

//Msg_1.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include<stdio.h>
#include<string.h>


struct msgbuf {
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
  };

int main()
{
		
        key_t key;
        key = ftok(".", 1);
        printf("key=%x\n",key);//打印键值

        struct msgbuf readBuf;
        struct msgbuf sendBuf = {888,"I will be successful"};

        int msgId = msgget(key,IPC_CREAT|0777);
        if(msgId == -1)
        {
                printf("get que failed\n");

        }

        msgrcv(msgId,&readBuf,sizeof(readBuf.mtext),666,0);

        msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);
        printf("get:%s\n",readBuf.mtext);
        
		//int msgctl(int msqid, int cmd, struct msqid_ds *buf);
		msgctl(msgId,IPC_RMID, NULL);//把消息队列从内核移除

        return 0;
}
//Msg_2.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include<stdio.h>
#include<string.h>


struct msgbuf {
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
  };

int main()
{

        key_t key;
        key = ftok(".", 1);
        printf("key=%x\n",key);



        struct msgbuf readBuf;
        struct msgbuf sendBuf = {666,"thank you for reaching"};

        int msgId = msgget(key,IPC_CREAT|0777);
        if(msgId == -1)
        {
                printf("get que failed\n");
        }
        msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);
        msgrcv(msgId,&readBuf,sizeof(readBuf.mtext),888,0);

        printf("get:%s\n",readBuf.mtext);
        
        msgctl(msgId,IPC_RMID, NULL);//把消息队列从内核移除
        return 0;
}

输出结果:
在这里插入图片描述

10、共享内存概述

共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。

特点:

  • 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
  • 因为多个进程可以同时操作,所以需要进行同步。
  • 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

原型:

 #include <sys/shm.h>
 // 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
 int shmget(key_t key, size_t size, int flag);
 // 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
 void *shmat(int shm_id, const void *addr, int flag);
 // 断开与共享内存的连接:成功返回0,失败返回-1
 int shmdt(void *addr); 
 // 控制共享内存的相关信息:成功返回0,失败返回-1
 int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

11、共享内存编程实现

  • 测试代码:
//shmw.c
#include <sys/ipc.h>
#include <sys/shm.h>
#include<stdio.h>
#include<stdlib.h>
 #include <unistd.h>
#include<string.h>
int main()
{
        int shmid;
        char *shmaddr;
		
		//定义key
        key_t key;
        key = ftok(".", 1);
        shmid = shmget(key,1024*4,IPC_CREAT|0666);//创建共享内存,共享内存大小以1024对齐
        
        //返回-1则创建失败
        if(shmid == -1)
        {
                printf("shmget failed\n");
                exit(-1);//异常结束,返回值-1
        }

        shmaddr = shmat(shmid,0,0);//挂接共享内存

        printf("shmat is ok\n");
        strcpy(shmaddr,"I will be successful");//把数据映射到内存上,也就是复制

        sleep(5);//睡眠5秒,等待读取数据结束
        
        shmdt(shmaddr);//断开与共享内存的连接
        shmctl(shmid,IPC_RMID,0);//去掉共享内存

        printf("quit\n");
        return 0;
}

//shmr.c
#include <sys/ipc.h>
#include <sys/shm.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
        int shmid;
        char *shmaddr;

        key_t key;
        key = ftok(".", 1);

        shmid = shmget(key,1024*4,0666);
        if(shmid == -1)
        {
                printf("shmget failed\n");
                exit(-1);
        }

        shmaddr = shmat(shmid,0,0);

        printf("shmat is ok\n");
        printf("data: %s\n",shmaddr);//打印写入内存中的数据


        shmdt(shmaddr);//断开与共享内存的连接


        printf("quit\n");
        return 0;
}

输出结果:
在这里插入图片描述
查看系统中创建的共享内存

ipcs -m

上面例子中创建的共享内存
在这里插入图片描述

删除系统中的共享内存

ipcrm -m 共享内存id号

12、信号概述信号的名字和编号:

  • 信号的名字和编号:
    每个信号都有一个名字和编号,这些名字都以**“SIG**”开头,例如**“SIGIO ”“SIGCHLD”等等。
    信号定义在
    signal.h头文件中,信号名都定义为正整数。
    具体的信号名称可以使用
    kill -l**来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0又特殊的应用。
    在这里插入图片描述
    信号的处理:
    信号的处理有三种方法,分别是:忽略、捕捉和默认动作

  • 忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是
    SIGKILLSIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景

  • 捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。

  • 系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。
    具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。

其实对于常用的 kill 命令就是一个发送信号的工具,kill 9 PID来杀死进程。比如,我在后台运行了一个 a.out,通过 ps 命令可以查看他的 PID,通过 kill 9 来发送了一个终止进程的信号来结束了 top 进程。如果查看信号编号和名称,可以发现9对应的是 9) SIGKILL,正是杀死该进程的信号。而以下的执行过程实际也就是执行了9号信号的默认动作——杀死进程

在这里插入图片描述

13、信号编程

  • 键盘按下ctrl+c,捕捉信号后,不能退出程序
//signal.c
#include <signal.h>
#include<stdio.h>

//       typedef void (*sighandler_t)(int);
//       sighandler_t signal(int signum, sighandler_t handler);

//自定义的信号处理函数
void sighandler(int signum)
{

        printf("get signum = %d\n",signum);//打印signum
        
        switch(signum)
        {
                case 2:
                        printf("SIGINT\n");
                        break;

                case 9:
                        printf("SIGKILL\n");
                        break;
        }

        printf("never quit\n");

}


int main()
{
        signal(SIGINT,sighandler);
        signal(SIGKILL,sighandler);//SIGKILL不能被忽略
        while(1);
        return 0;
}

输出结果:
在这里插入图片描述

  • 编程实现kill发送信号
#include <sys/types.h>
#include <signal.h>
#include<stdio.h>
#include <stdlib.h>

//       int kill(pid_t pid, int sig);

int main(int argc,char **argv)
{
        int signum;
        int pid;//id

        signum = atoi(argv[1]);
        pid = atoi(argv[2]);

        printf("num = %d,pid = %d\n",signum,pid);

        kill(pid,signum);
        printf("send signal is ok\n");

        return 0;
}

输出结果:
在这里插入图片描述
用system函数来发送信号

#include <sys/types.h>
#include <signal.h>
#include<stdio.h>
#include <stdlib.h>

//       int kill(pid_t pid, int sig);

int main(int argc,char **argv)
{
        int signum;
        int pid;
        char cmd[128] = {0};

        signum = atoi(argv[1]);
        pid = atoi(argv[2]);

        printf("num = %d,pid = %d\n",signum,pid);

        sprintf(cmd,"kill -%d %d",signum,pid);
        
       /*
        #include <stdlib.h>
       	int system(const char *command);*/

        system(cmd);
        printf("send signal is ok\n");

        return 0;
}

输出结果:
在这里插入图片描述

14、信号如何携带消息

信号注册函数——高级版
我们已经成功完成了信号的收发,那么为什么会有高级版出现呢?其实之前的信号存在一个问题就是,虽然发送和接收到了信号,可是总感觉少些什么,既然都已经把信号发送过去了,为何不能再携带一些数据呢?
正是如此,我们需要另外的函数来通过信号传递的过程中,携带一些数据。

15、信号携带消息编程实战

//signal_rec.c
#include<signal.h>
#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>

void handler(int signum,siginfo_t *info,void *context)
{
        printf("get signum %d\n",signum);

        if(context != NULL)
        {
                printf("get data = %d\n",info->si_int);
                printf("get data = %d\n",info->si_value.sival_int);
                printf("from:%d\n",info->si_pid);
        }


}
int main()
{
        struct sigaction act;
        printf("pid = %d\n",getpid());

        act.sa_sigaction = handler;
        act.sa_flags = SA_SIGINFO;

        sigaction(SIGUSR1,&act,NULL);
        while(1);

        return 0;
}
~ 
//signal_sd.c
#include<signal.h>
#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>
 #include <stdlib.h>

int main(int argc,char **argv)
{
        int signum;
        int pid;

        signum = atoi(argv[1]);
        pid = atoi(argv[2]);

        union sigval value;
        value.sival_int = 100;

        sigqueue(pid,signum,value);
        printf("%d,done\n",getpid());

        return 0;
}

输出结果:
在这里插入图片描述

16、信号量概述

**信号量(semaphore)**与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

特点

  • 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
  • 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
  • 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
  • 支持信号量组。

原型
最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。

Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。

#include <sys/sem.h>
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);  
 // 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);

17、信号量编程实现

  • 测试代码:
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include <unistd.h>

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 pGetKey(int id)
{

        struct sembuf set;
        set.sem_num = 0;
        set.sem_op = -1;
        set.sem_flg = SEM_UNDO;

        semop(id,&set,1);
        printf("get key\n");


}
void vPutBackKey(int id)
{

        struct sembuf set;
        set.sem_num = 0;
        set.sem_op = 1;
        set.sem_flg = SEM_UNDO;

        semop(id,&set,1);
        printf("put back the key\n");
}
int main(int argc,char const *argv[])
{

        key_t key;
        int semid;

        key = ftok(".",2);
        
        semid = semget(key,1,IPC_CREAT|0666);//获取/创建信号量

        union semun initsem;
        initsem.val = 0;

        semctl(semid,0,SETVAL,initsem);

        int pid = fork();
        if(pid > 0)
        {

                pGetKey(semid);//去拿锁
                sleep(1);
                printf("this is father's process\n");
                vPutBackKey(semid);//锁放回去
                semctl(semid,0,IPC_RMID);
        }
        else if(pid == 0)
        {
                printf("this is child's process\n");
                vPutBackKey(semid);

        }
        else{
                printf("fork error\n");
        }

        return 0;
}
                 

输出结果:

在这里插入图片描述

二、往期文章

1、入门Linux系统编程–文件
2、入门Linux系统编程–进程
3、入门Linux系统编程–线程
4、入门Linux系统编程–进程间通信
5、入门Linux系统编程–网络编程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秃秃秃秃哇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值