【Linux系统编程(IPC)】无名管道,命名管道,消息队列,共享内存,信号,信号量,Socket

概述

进程间通信:IPC(InterProcess Communication)
微信的聊天也是属于进程间通信,是基于网络的。
本篇的进程间通信基于单机的。

A进程和B进程之间创建一个通道,A和B都能从中读写数据,这是真正意义上的进程间通信。
进程间通信:
● 单机版:A和B跑在同一个PC上
● 多机版:A和B在不同的PC上(基于网络通信)

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

一、无名管道

管道
管道,通常指无名管道,是UNIX系统IPC最古老的形式。
特点:

  1. 它是半双工的,具有固定的读端和写端
  2. 它只能用于具有亲缘关系的进程间通信(父子进程或兄弟进程之间)。
  3. 它可以看成一种特殊的文件,对于它的读写可以使用普通的read、write函数。但它不是普通的文件,它不属于任何文件系统,并且只存在于内存
    同一时间,单向流动。读走就没

【看成文件,但不是文件,而是在内存中】【有名管道是文件】
【看成文件,就可以用read和write】

原型:

#include <unistd.h>
int pipe(int pipefd[2]);

On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
当一个管道建立时,会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。
在这里插入图片描述

需要关闭管道,只需要把这两个文件描述符关闭即可。
管道在内核中开辟空间被创建。

注:

read函数,没有数据,会阻塞。

无名管道通信前,需要一方关闭读,另一方关闭写

read 没有 阻塞 像一个水管

代码:

管道编程,读时候关闭写,写时候关闭读。这个有点残疾,

当管道中没有数据时候,read函数会阻塞,直到父进程写入数据,子进程继续执行。

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

int main()
{

	int fd[2];
	char buf[128];
	int pid;
	
	//int pipe(int pipefd[2]);
	if( pipe(fd) == -1)
	{
		printf("creat pipe failed\n");
	}


	if( (pid = fork()) < 0)
	{
		printf("create pro failed\n");

	}
	else if (pid == 0)
	{
		printf("this is chlid pro\n");
		close(fd[1]);
		//ssize_t read(int fd, void *buf, size_t count);
		read(fd[0], buf, sizeof(buf));
		printf("child read:%s\n", buf);
		exit(0);	
	}
	else
	{//父进程
		sleep(3);//这3秒 子进程阻塞
		printf("this is parent pro\n");
		close(fd[0]);
		//ssize_t write(int fd, const void *buf, size_t count);
		write(fd[1], "from father to child", sizeof("from father to child"));
		wait(NULL);
	}

	return 0;
}

读和写一开始就确定,一般不修改,无名管道存储在内存中,非磁盘

这是管道,无名管道。

下面说命名管道。

二、命名管道

FIFO
FIFO,也称命名管道,它是一种文件类型。

特点

  1. FIFO可以在无关的进程间交换数据,与无名管道不同。
  2. FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

原型

在man手册第三页

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

其中的mode参数与open函数中的mode相同。一旦创建了一个FIFO,就可以使用一般文件I/O函数操作它。
当open一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:【默认是阻塞】

  • 若没有指定O_NONBLOCK(默认),只读open要阻塞到某个其它进程为写而打开此FIFO。类似的,只写的open要阻塞到某个其它进程为读而打开它。
  • 若指定了O_NONBLOCK,则只读open立即返回。而只写open将出错返回 -1 如果没有进程已经为读而打开该FIFO,其 errno 置 ENXIO。

RETURN VALUE
On success mkfifo() and mkfifoat() return 0. In the case of an error, -1 is returned (in
which case, errno is set appropriately).

例子

调用mkfifo
在这里插入图片描述

p类型文件 、可读可写权限

mkfifo的返回值

RETURN VALUE
成功返回0,失败返回-1。
在这里插入图片描述

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>

//int mkfifo(const char *pathname, mode_t mode);

int main()
{
	int ret = mkfifo("./file", 0600);

	if(ret == 0){
		printf("mkfifo success\n");
	}
	if(ret == -1){
		printf("mkfifo failed\n");
	}

	return 0;
}

修整一下返回值问题:成功失败都显示

命名管道文件已存在的情况
在这里插入图片描述

如下程序,当文件存在时,显示创建失败和失败原因。文件不存在时,显示创建成功。

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

int main()
{
	if( mkfifo("./file", 0600) == -1 && errno == EEXIST){
		printf("mkfifo failed\n");
		perror("why:");
	}else{
		if(errno == EEXIST){
			printf("file exist\n");
		}else{
			printf("mkfifo success\n");
		}
	}

	return 0;
}

不显示文件是否存在信息,显示其它错误信息

上一程序,如遇到文件存在之外的错误,也会输出 mkfifo success。做如下调整。

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

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

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

#include <fcntl.h> 是O_RDONLY的库
程序运行,但没动(阻塞)

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

int main()
{
	if( mkfifo("./file", 0600) == -1 && errno != EEXIST){
	
		printf("mkfifo failed\n");
		perror("why:");
	}
	
	//int open(const char *pathname, int flags);
	open("./file", O_RDONLY);
	printf("open success\n");
	
	return 0;
}

在这里插入图片描述

所以,

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

int main()
{
        if( mkfifo("./file", 0600) == -1 && errno != EEXIST){

                printf("mkfifo failed\n");
                perror("why:");
        }

        //int open(const char *pathname, int flags);
        open("./file", O_RDONLY);
        printf("open success\n");

        return 0;
}
//writedemo6
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdlib.h>
#include <fcntl.h>
//int mkfifo(const char *pathname, mode_t mode);

int main()
{
        open("./file", O_WRONLY);
        printf("open success\n");

        return 0;
}

先调用read,会阻塞。这时候,调用write,进程以写打开才继续走
在这里插入图片描述

使用fifo读写数据

在这里插入图片描述

//demo6.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
//int mkfifo(const char *pathname, mode_t mode);

int main()
{
        char buf[32] = {0};
        int fd;
        int nread;

        if( mkfifo("./file", 0600) == -1 && errno != EEXIST){

                printf("mkfifo failed\n");
                perror("why:");
        }

        //int open(const char *pathname, int flags);
        fd  = open("./file", O_RDONLY);
        printf("open success\n");

        while(1){

                nread = read(fd, buf, sizeof(buf));
                printf("read %d bytes from fifo, content: %s\n", nread, buf);
        }

        close(fd);
        return 0;
}
//writedemo6.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
//int mkfifo(const char *pathname, mode_t mode);

int main()
{
        int fd;
        int cnt = 0;
        char *str = "i come from writefifo";

        fd = open("./file", O_WRONLY);

        while(1){
                sleep(1);
                if(cnt == 5) {
                        break;
                }
                write(fd, str, strlen(str));
                cnt++;
        }

        printf("open success\n");

        close(fd);
        return 0;
}

三、消息队列通信原理

原型及特点

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

特点

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

原型


/*
key是一个索引值,通过key这个索引值在内核中找到某个队列
flag是打开队列的方式,
返回一个int,是队列ID。
*/
int msgget(key_t key, int msgflg);

/** 添加消息
消息ID 消息 消息大小 标志位 
*/
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

/* 读取消息
消息ID 消息 消息大小 队列类型 标志位
*/
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

/* 控制消息队列
消息ID
*/
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

一下两种情况下,msgget将创建一个新的消息队列:
● 如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。
● key参数为IPC_PRIVATE。
第一种方式比第二种方式常用,因为两个进程同时使用就不能是私有。

函数msgrcv在读取消息队列时,type参数有下面几种情况:
● type == 0,返回队列中的第一个消息;
● type > 0,返回队列中消息类型为 type 的第一个消息;
● type < 0,返回队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。

消息队列编程收发数据

第一行:如果有队列就直接获取,没有就创建队列
msgrcv最后一个参数为0,是默认,就是读不到数据就阻塞
信息定义类似下面:

在这里插入图片描述

在这里插入图片描述

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

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


int main()
{
	struct msgbuf readBuf;

	//1.get queue
	// key是一个索引值,通过key这个索引值在内核中找到某个队列
	//int msgget(key_t key, int msgflg);
	int msgId = msgget(0x1234, IPC_CREAT|0777);
	// key随便写
	// 0777权限
	
	if(msgId == -1){
		printf("get queue fail\n");
	}

	//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
	msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 888, 0);
    //第三个参数是结构体中mtext的空间大小
    //第四个参数 888 是消息种类,接收、发送两端须一致
    //msgrcv最后一个参数为0,是默认,就是读不到数据就阻塞
	printf("read from que: %s\n", readBuf.mtext);

	return 0;
}
//msgSend.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf {
       long mtype;       /* message type, must be > 0 */
       char mtext[128];    /* message data */
};

int main()
{
	struct msgbuf sendBuf = {888, "this message from quene"};

	//1.get queue
	int msgId = msgget(0x1234, IPC_CREAT|0777);//通过同样的索引值找到消息队列
	
	if(msgId == -1){
		printf("get queue fail\n");
	}

	//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
	msgsnd(msgId, &sendBuf, strlen(sendBuf.mtext), 0);
    //最后一个参数为0,是默认,就是读不到数据就阻塞
	return 0;
}

两端收的同时也发:
在这里插入图片描述

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

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


int main()
{
	struct msgbuf readBuf;

	//1.get queue
	
	//int msgget(key_t key, int msgflg);
	int msgId = msgget(0x1234, IPC_CREAT|0777);
	// key随便写
	// 0777权限
	
	if(msgId == -1){
		printf("get queue fail\n");
	}

	//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
	msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 888, 0);
	printf("read from que: %s\n", readBuf.mtext);

	struct msgbuf sendBuf = {666, "thank your reach"};
	msgsnd(msgId, &sendBuf, strlen(sendBuf.mtext), 0);
	return 0;
}
//msgSend.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf {
       long mtype;       /* message type, must be > 0 */
       char mtext[128];    /* message data */
};

int main()
{
	struct msgbuf sendBuf = {888, "this message from quene"};

	//1.get queue
	int msgId = msgget(0x1234, IPC_CREAT|0777);//通过同样的索引值找到消息队列
	
	if(msgId == -1){
		printf("get queue fail\n");
	}

	//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
	msgsnd(msgId, &sendBuf, strlen(sendBuf.mtext), 0);

	struct msgbuf readBuf;
	msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 666, 0);
    printf("read from que: %s\n", readBuf.mtext);
	return 0;
}

键值生成及消息队列移除

ftok

msgctl

ftok和删除消息队列的应用:

在这里插入图片描述

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

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


int main()
{
	struct msgbuf readBuf;

	//1.get queue

	key_t key;
	key = ftok(".", 1);
	printf("key=%x\n", key);
	//int msgget(key_t key, int msgflg);
	int msgId = msgget(key, IPC_CREAT|0777);
	// key随便写
	// 0777权限
	
	if(msgId == -1){
		printf("get queue fail\n");
	}

	//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
	msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 888, 0);
	printf("read from que: %s\n", readBuf.mtext);

	struct msgbuf sendBuf = {666, "thank your reach"};
	msgsnd(msgId, &sendBuf, strlen(sendBuf.mtext), 0);

	//int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    //用IPC_RMID指令 删除该key消息队列
	msgctl(msgId, IPC_RMID, NULL);
	return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf {
       long mtype;       /* message type, must be > 0 */
       char mtext[128];    /* message data */
};

int main()
{
	struct msgbuf sendBuf = {888, "this message from quene"};

	key_t key;
    key = ftok(".", 1);
	printf("key=%x\n", key);
    //int msgget(key_t key, int msgflg);
    int msgId = msgget(key, IPC_CREAT|0777);
	
	if(msgId == -1){
		printf("get queue fail\n");
	}

	//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
	msgsnd(msgId, &sendBuf, strlen(sendBuf.mtext), 0);

	struct msgbuf readBuf;
	msgrcv(msgId, &readBuf, sizeof(readBuf.mtext), 666, 0);
    printf("read from que: %s\n", readBuf.mtext);
	msgctl(msgId, IPC_RMID, NULL);//调用RMID指令删除对应key的消息队列
	return 0;
}

四、共享内存

共享内存概述

IPC的方式有几种:

  1. 无名管道
  2. 命名管道
  3. 消息队列
  4. 共享内存:前四种中,比较先进,使用较多的一种方式。
  5. 信号
  6. 信号量
    信号量不作为IPC的一种方式,它更像去控制一种临界资源。
    在这里插入图片描述

管道:男的放入纸条,女的取走纸条。单向,残疾
消息队列:桌子上有箱子,男的把纸条放入箱子,女的看纸条不拿走。女的也能放入纸条,男的能看不拿走。
共享内存:比较高级,桌子上一张纸。男的写完,女的直接就看到。女的写完男的直接看到。

共享内存编程步骤:

  1. 创建/打开 共享内存
  2. 映射
  3. 数据
  4. 释放共享内存
  5. 干掉

共享内存编程实现

原型:

#include <sys/shm.h>

// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int shmflg);

// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void shmat(int shmid, const void *shmaddr, int shmflag);
//第一个参数是我们获取共享内存的ID
//第二个参数一般写0让Linux内核自动安排共享内存
//第三个参数一般我们也写0,代表映射进来的共享内存可读可写

// 断开与共享内存的连接:成功返回0,失败返回-1
int shmdt(const void *shmaddr);

// 控制共享内存的相关信息:成功返回0,失败返回-1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//第一个参数共享内存是共享内存ID
//第二个参数是指令(可查man手册查看),类似消息队列
//第三个是卸载共享内存时候产生的一些信息,我们不关心这些信息,写NULL

共享内存大小必须以兆为单位。
正常退出 0 异常 -1

一般步骤:

  1. 创建或者获取
  2. 映射
  3. 读/写
  4. 卸载(断开进程和共享内存的联系)
  5. 挂掉(挂掉,避免占用空间)

例子:

//shmw.c
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
        int shmid;
        key_t key;
        key = ftok(".", 1);

        //int shmget(key_t key, size_t size, int shmflg);
        shmid = shmget(key, 1024*4, IPC_CREAT|0666);//大小以M为单位

        if(shmid == -1){
                printf("shmget fail\n");
                exit(-1);
        }

        //获取之后,映射共享内存

        //定义一个变量指向共享内存
        char *shmaddr;
        //void *shmat(int shmid, const void *shmaddr, int shmflg);      
        shmaddr = shmat(shmid, 0, 0);//到此,完成映射

        printf("shmat ok\n");
        //向共享内存中写数据
        strcpy(shmaddr, "changx");
        sleep(5);
        //使用共享内存完毕,卸载共享内存
        //int shmdt(const void *shmaddr);
        shmdt(shmaddr);

        //挂掉共享内存,免得占用空间
        //int shmctl(int shmid, int cmd, struct shmid_ds *buf);
        shmctl(shmid, IPC_RMID, 0);

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

有了写之后,还需要读。读的时候不需要创建,直接获取,因此,shmget的第三个参数写为0。
读取后,卸载共享内存。写那边有删除,这里就不删除了。

#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
        int shmid;
        key_t key;
        key = ftok(".", 1);

        //int shmget(key_t key, size_t size, int shmflg);
        shmid = shmget(key, 1024*4, 0);//大小以M为单位

        if(shmid == -1){
                printf("shmget fail\n");
                exit(-1);
        }

        //获取之后,映射共享内存

        //定义一个变量指向共享内存
        char *shmaddr;
        //void *shmat(int shmid, const void *shmaddr, int shmflg);      
        shmaddr = shmat(shmid, 0, 0);//到此,完成映射

        printf("shmat ok\n");
        printf("data:%s\n", shmaddr);

        shmdt(shmaddr);//断开进程和共享内存的联系

        //挂掉共享内存,免得占用空间
        //int shmctl(int shmid, int cmd, struct shmid_ds *buf);
        //shmctl(shmid, IPC_RMID, 0);

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

如何查看操作系统中有哪些共享内存:

ipcs -m //查看
ipcrm -m shmid(共享内存id) //删除某个id的共享内存

共享内存不支持原子操作(两个进程同时写入,就很容易乱)。可以利用信号量控制。

若是刚创建完共享内存就退出程序,查看操作系统中有时共享内存会多出一个。

五、信号

5.1 信号概述

对于Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。信号,为Linux提供了一种处理异步事件的方法。比如,终端用户出入了 ctrl c 来中断程序,会通过信号机制停止一个程序。

单片机的中断,串口有数据,硬件处理中断。

信号的名字和编号

每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO”,“SIGCHID”等等。信号定义在 signal.h 中,信号名都定义为正整数。

具体的信号名称可以使用 kill -l 来查看信号名字以及序号,信号是从 1 开始编号的,不存在 0 的编号。kill对于信号 0 有特殊的应用。从应用层看编号是从1到64。

在这里插入图片描述

信号的处理:

信号的处理有三种方式,分别是:忽略、捕捉、默认动作。

  1. 忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILL 和 SIGSTOP )。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的进程,这显然是内核设计者不希望看到的场景。
  2. 捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号的处理函数,然后将这个函数告诉给内核。当信号产生时,由内核来调用用户自定义的函数,由此来实现某种函数的处理。
  3. 系统默认动作,对于每个信号来说,系统都有默认的处理动作,当发生了该信号,系统便会自动执行。不对,对于系统来说,多数处理方式比较粗暴,就是直接杀死进程。具体的信号默认动作可以使用 man 7 signal 来查看系统的具体定义。

了解了信号的概述,那么信号是如何使用的呢?

其实对于常用的 kill 命令就是一个发送信号的工具,kill 9 PID来杀死进程。

在这里插入图片描述
两种都可以(使用信号编号 或者 使用信号名字)

对于信号来说,最大的意义不是为了杀死信号,而是实现一些异步通讯的手段。所谓的实现异步通讯就是信号处理的第二种:捕捉信号

5.2 信号编程

信号处理函数的注册
信号处理函数的注册不只一种方法,分为入门版和高级版。

  1. 入门版:函数 signal
  2. 高级版:函数 sigaction

信号处理发送函数
信号发送函数也不只一个,同样分为入门版和高级版

  1. 入门版: kill
  2. 高级版: sigqueue
#include <signal.h>

// void (*sighandler_t)(int) 是函数指针
// 函数没有返回
// 函数名是 sighandler_t
// 函数的参数是 一个整形参数
typedef void (*sighandler_t)(int);

// 返回值是 sighandler_t类型
// 第一个参数是 要捕捉哪个信号(信号编号,上个图中的,kill -l查询)
// 第二个参数是 上一行代码中的函数指针, (小声:_t是结构体的意思)
sighandler_t signal(int signum, sighandler_t handler);
// handler就是一个函数指针,指向void (*sighandler_t)(int)类型的函数,因为用typedef定义了类型

5.3 信号的绑定

实现 捕捉信号 。键盘按下CTRL C 不能终止程序 没办法停止程序

ctrl+c的默认动作是终止进行,现在我已经捕捉信号,修改默认动作,执行我希望它执行的函数。

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

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

void handler(int signum)
{
	printf("\nget signum=%d\n", signum);//打印信号的值
	printf("never quit\n");
}

int main()
{
	signal(SIGINT, handler);//这样就完成了信号的注册 kill -l查询第一个参数
	while(1);
	return 0;
}

按CTRL C的效果:不能退出
在这里插入图片描述

增加了捕获其它信号,但是SIGKILL是不能捕捉的。

代码如下:

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

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

void handler(int signum)
{
	printf("\nget signum=%d\n", signum);//打印信号的i值
	switch(signum){
		case 2:
			printf("SIGINT\n");
			break;
		case 9:
			printf("SIGKILL\n");
			break;
		case 10:
			printf("SIGUSR1");
			break;
	}
	//printf("never quit\n");
}

int main()
{
	signal(SIGINT, handler);//这样就完成了信号的注册
	signal(SIGKILL, handler);
	signal(SIGUSR1, handler);

	while(1);
	return 0;
}

捕获信号的效果:
在这里插入图片描述

5.4 信号的发送

通过指令发送信号,能不能通过程序发送信号?能!通过 kill 函数。

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;

	signum = atoi(argv[1]);//获取信号编号
	pid = atoi(argv[2]);//获取进程号

	printf("num=%d, pid=%d\n", signum, pid);
	
	kill(pid, signum);//发送信号
	printf("send signal ok");

	return 0;
}

效果:
在这里插入图片描述

另外一种实现方式:领用system函数调用脚本。实际上就是调用指令嘛。
在这里插入图片描述

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

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);//格式化字符串
	system(cmd);
	//kill(pid, signum);
	printf("send signal ok");

	return 0;
}

5.5 信号的忽略

KILL是无法忽略的


如何忽略信号?
man手册中有
输入如下

man 2 signal

然后搜索 SIG 开头的
在这里插入图片描述

编码:(忽略信号)

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

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

void handler(int signum)
{
        printf("\nget signum=%d\n", signum);//打印信号的i值
        switch(signum){
                case 2:
                        printf("SIGINT\n");
                        break;
                case 9:
                        printf("SIGKILL\n");
                        break;
                case 10:
                        printf("SIGUSR1");
                        break;
        }
        //printf("never quit\n");
}

int main()
{
        signal(SIGINT, SIG_IGN);//这样就完成了信号的注册
        signal(SIGKILL, SIG_IGN);//SIG_IGN是忽略该信号的宏
        signal(SIGUSR1, SIG_IGN);

        while(1);
        return 0;
}

下图,按了两个 CTRL+C 没反应,因为代码中我们忽略了该信号。需要注意的是:如果使用kill指令杀死该进程还是能够杀死的,虽然对9的信号编码进行了信号忽略处理,但还是不能杀死该进程。
在这里插入图片描述

5.6 高级版本的(能携带消息了)如何携带消息

入门的重点在于发信号的动作,不会携带消息。就比如女朋友在外边敲门,男朋友在室内做出响应。高级的API就相当于,女朋友在敲门的同时也说话(你快给我开门,不然我TM干死你),也就是发送信号的同时也传递了其它的信息。手动狗头!

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

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//第一个参数是信号编码
//第二个参数是结构体 struct sigaction(结构体中:函数指针1 函数指针2 结构体 整型数)
//第三个参数是结构体 struct sigaction 备份原操作

struct sigaction {
   void       (*sa_handler)(int); // 和signal的第二个参数一样 //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
   void       (*sa_sigaction)(int, siginfo_t *, void *); //能额外接收参数 //信号处理程序,能够接受额外数据和sigqueue配合使用
                //int 			参数:
                //siginfo_t * 	参数:(pid谁发的,si_int数据,,si_value(int char*)数据)
                //void *		参数:为空代表无数据;非空代表有数据;
   sigset_t   sa_mask;	//处理这个信号起到一个阻塞的作用,我处理这个信号的时候不处理其它信号。有新信号让它排队//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。
   int        sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据
 };
//回调函数句柄sa_handler、sa_sigaction只能任选其一

siginfo_t {
               int      si_signo;    /* Signal number */
               int      si_errno;    /* An errno value */
               int      si_code;     /* Signal code */
               int      si_trapno;   /* Trap number that caused
                                        hardware-generated signal
                                        (unused on most architectures) */
               pid_t    si_pid;      /* Sending process ID */
               uid_t    si_uid;      /* Real user ID of sending process */
               int      si_status;   /* Exit value or signal */
               clock_t  si_utime;    /* User time consumed */
               clock_t  si_stime;    /* System time consumed */
               sigval_t si_value;    /* Signal value */
               int      si_int;      /* POSIX.1b signal */
               void    *si_ptr;      /* POSIX.1b signal */
               int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
               int      si_timerid;  /* Timer ID; POSIX.1b timers */
               void    *si_addr;     /* Memory location which caused fault */
               int      si_band;     /* Band event */
               int      si_fd;       /* File descriptor */
}

union sigval {
   int   sival_int;
   void *sival_ptr;
};

信号发送函数

#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval value);
//第一个参数:发给谁
//第二个参数:发的是什么信号
//第三个参数:消息(类型为 int 或 char *)

union sigval {
   int   sival_int;
   void *sival_ptr;
};

接收信号的代码:

//NiceSignal.c
#include <signal.h>
#include <stdio.h>

//int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

void handler(int signum, siginfo_t *info, void *context)
{
        //读取:把数据拿出来
        printf("get signum:%d\n", signum);
        //内容是否有 取决于context是否为空
        if(context != NULL){
                //需要的话,把pid等其他数据也能拿出来,通过
                printf("get data = %d\n", info->si_int);
                printf("get data = %d\n", info->si_value.sival_int);
        }
}

int main()
{
        struct sigaction act;

        act.sa_sigaction = handler;//接受信号后调用handler来处理信号
        act.sa_flags = SA_SIGINFO;//目的:能获取消息

        sigaction(SIGUSR1, &act, NULL);//注册信号 参数1:接收那个信号 2想干嘛 3用来备份
        while(1);

        return 0;
}

发送代码:

//send.c
#include <signal.h>
#include <stdio.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;
       //int sigqueue(pid_t pid, int sig, const union sigval value);
       sigqueue(pid, signum, value);
       printf("done\n");
}
/*
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);

//第三个参数 联合体
union sigval {
               int   sival_int;
               void *sival_ptr;
};*/

gcc send.c -o send
gcc NiceSignal.c -o pro

运行pro
在这里插入图片描述

查看进程pro的pid
在这里插入图片描述

运行send
在这里插入图片描述

成功利用程序发送信号
在这里插入图片描述


改进版:直接显示pid,无需使用命令查询进程pro的pid
在这里插入图片描述
收(捕获)

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

//int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//


void handler(int signum, siginfo_t *info, void *context)
{
        //读取:把数据拿出来
        printf("get signum:%d\n", signum);
        //内容是否有 取决于context是否为空
        if(context != NULL){
                //需要的话,把pid等其他数据也能拿出来,通过
                printf("get data = %d\n", info->si_int);
                printf("get data = %d\n", info->si_value.sival_int);
                printf("get from pid:%d\n", info->si_pid);
        }
}

int main()
{
        struct sigaction act;

        act.sa_sigaction = handler;//接受信号后调用handler来处理信号
        act.sa_flags = SA_SIGINFO;//目的:能获取消息

        printf("pid:%d\n", getpid());

        sigaction(SIGUSR1, &act, NULL);//注册信号 参数1:接收那个信号 2想干嘛 3用来备份
        while(1);

        return 0;
}

发(发送)

//send.c
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.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;
       //int sigqueue(pid_t pid, int sig, const union sigval value);
       sigqueue(pid, signum, value);
       printf("pid:%d, done\n", getpid());

}

六、信号量

6.1 信号量概述(和信号一点关系没有)

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

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

进程间通信方式:

  1. 无名管道
  2. 有名管道
  3. 消息队列(双向)
  4. 共享内存(双向)
  5. 信号
  6. 信号量(不涉及数据,管理临界资源)

信号量集:Linux下不只是一个信号量
拿锁的过程是 P操作
放回锁的过程是 V操作

最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。
Linux下的信号量函数都是在通用的信号量组上进行操作,而不是在一个单一的二值信号量上进行操作。

原型:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

//创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int nsems, int semflg);

//对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf *sops, size_t nsops);

//控制信号量的相关信息
int semctl(int semid, int semnum, int cmd, ...);

6.2 信号量编程实现一

代码:

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

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

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) */
};

int main()
{
        key_t key;
        int semid;

        key = ftok(".", 2);//

        //获取/创建信号量 信号量中有一个信号量
        semid = semget(key, 1, IPC_CREAT|0666);//key,信号量集中信号量的个数,没有是否创建

        union semun initsem;
        initsem.val = 1;// 1把锁 有钥匙

        //初始化信号量 操作第0个信号量
        //SETVAL设置信号量的值,设置为initsem
        //int semctl(int semid, int semnum, int cmd, ...);
        semctl(semid, 0, SETVAL, initsem);//信号量集的ID,操作第几个信号量,控制信号量集合的方式


        int pid = fork();
        if(pid > 0){
                //拿锁
                printf("this is father\n");
                //把锁放回去 
        } else if(pid == 0){

                printf("this is child\n");

        } else {

                printf("fork fail\n");

        }

        return 0;
}

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

基本都是父进程先执行,也有子进程先执行。能不能让子进程先执行呢?能!
之前方法:

  1. 父进程调用wait 等待子进程
  2. 父进程调用sleep
  3. 下节使用 PV操作(如何获取/归还信号量)

6.3 信号量编程实现二

目的:让子进程先执行

封装两个函数,P操作和 V操作

取钥匙函数
在这里插入图片描述

信号量集有多个信号,需要定义数组。一个信号量就不用定义数组,直接定义变量就可以。

在这里插入图片描述

在sem_flg中可以识别的标志是IPC_NOWAIT和SEM_UNDO。如果某个操作指定了SEM_UNDO,那么在进程结束时,该操作将自动撤销。如果是IPC_NOWAIT就失去了意义,因为父子进程执行顺序还不是固定的。

刚开始是无锁的状态,想让谁先执行,谁就执行完才放锁,不需要拿锁。其它进程拿锁,先执行的不放锁,其它进程就不能执行。
这样保证子进程先执行。

代码:

//sem.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

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

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) */
};

//自定义拿锁 P操作
void pGetKey(int id){

        //int semop(int semid, struct sembuf *sops, size_t nsops);
        //参数一是ID
        //参数二是配置的信号量
        //参数三是从第二项开始的个数
        
        struct sembuf set;
        set.sem_num = 0;        //信号编号     /* Operate on semaphore 0 */
        set.sem_op = -1;        //拿钥匙 -1         /* Wait for value to equal 0 */
        set.sem_flg = SEM_UNDO; //等待????? 
        semop(id, &set, 1);//对从set地址开始的1个信号量操作
        printf("getKey\n");
}

//自定义还锁 V操作
void vPutBackKey(int id){

        //int semop(int semid, struct sembuf *sops, size_t nsops);
        //参数一是ID
        //参数二是配置的信号量
        //参数三是第二项的个数
        
        struct sembuf set;
        set.sem_num = 0;        //信号编号     /* Operate on semaphore 0 */
        set.sem_op = 1;         //放回钥匙 
        set.sem_flg = SEM_UNDO; //等待????? 
        semop(id, &set, 1);
        printf("put back the Key\n");
}

//主函数入口
int main()
{
        key_t key;
        int semid;

        key = ftok(".", 2);//

        //获取/创建信号量 信号量中有一个信号量
        semid = semget(key, 1, IPC_CREAT|0666);//key,信号量集中信号量的个数,没有是否创建

        union semun initsem;
        initsem.val = 0;// 不放锁 默认先执行的子进程已经拿到锁,目的是让子进程先执行

        //初始化信号量 操作第0个信号量
        //SETVAL设置信号量的值,设置为initsem
        //int semctl(int semid, int semnum, int cmd, ...);
        semctl(semid, 0, SETVAL, initsem);//信号量集的ID,操作第几个信号量,控制信号量集合的方式


        int pid = fork();
        if(pid > 0){
                //拿锁
                pGetKey(semid);
                printf("this is father\n");
                vPutBackKey(semid);
                //把锁放回去 

            	//销毁锁
                semctl(semid, 0, IPC_RMID);

        } else if(pid == 0){
                //pGetKey(semid);
                printf("this is child\n");
                vPutBackKey(semid);

        } else {

                printf("fork fail\n");

        }

        return 0;
}

效果:
在这里插入图片描述

  • 15
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值