Linux系统编程——进程间的通信

文章目录

  • 是什么进程间通信
  • 进程间通信的目的
  • 进程间的通信IPC
  • 各通信方式的比较和优缺点
  • 进程间通信方式的选择:
  • 无名管PIPE
    • 管道的读写
    • 父子进程使用管道通信实现 ps aux| grep "bash"
  • 命名管道 FIFO
    • 命名管道的创建
    • fifo 案例实现两进程通信
  • 消息队列
    • fork
    • 消息队列实现全双工通信
  • 共享内存
    • 共享内存实现两进程通信
  • 信号
    • 信号的概述
    • 信号发送函数
      • kill
      • raise
      • alarm
      • sigqueue 的函数原型
    • 信号处理函数
      • signal 的函数原型
      • sigaction 的函数原型
      • 利用信号实现进程间通信
  • 信号量

【转】进程间的五种通信方式

是什么进程间通信

每一个进程想要访问物理内存,都是通过访问进程虚拟地址空间当中的虚拟地址,借助页表的映射来访问的。这里的虚拟地址空间和页表都是进程级的,保证了进程之间的数据独立,不会相互干扰。但是,进程之间也是要相互合作的,简单的理解进程间通信就是多个进程对同一份公共资源进行操作,而通信最重要的前提是保证进程能看到同一份资源。

进程间通信的目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。例如xi
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程间的通信IPC

IPC 概念

  • Inter Process Communication 进程间通信。

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

  1. 管道通信:有名管道,无名管道
  2. 信号 - 系统开销小
  3. 消息队列 - 内核的链表
  4. 信号量 - 计数器
  5. 共享内存
  6. 内存映射
  7. 套接字

各通信方式的比较和优缺点

  • 管道:速度慢,容量有限,只有父子进程或兄弟进程能通讯,是半双工的通信,数据只能单向流动

  • FIFO:任何进程间都能通讯,但速度慢

  • 消息队列:

    • 容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。
    • 由系统调用函数来实现消息发送和接收之间的同步,从而使得用户在使用消息缓冲进行通信时不再需要考虑同步问题,使用方便。
  • 共享内存:能够很容易控制容量,速度快,但是要保持同步,比如一个进程在写的时候,另一个进程要注意读的问题。

  • 信号:不能传递复杂消息,用于通知接收进程某个事件已经发生。主要作为进程间以及同一进程不同线程之间的同步手段。通过给进程发送相应信号,该进程接收到此信号后运行信号处理函数或者作出默认或忽略回应

  • 套接字:可用于不同机器间的进程通信

进程间通信方式的选择:

  • PIPE 和 FIFO(有名管道) 用来实现进程间相互发送非常短小的、频率很高的消息,这两种方式通常适用于两个进程间的通信。
  • 共享内存用来实现进程间共享的、非常庞大的、读写操作频率很高的数据;这种方法适用于多进程间的通信。
  • 其他考虑用 socket 。主要应用在分布式开发中。

无名管PIPE

特点:

  1. 它是半双工的,具有固定的读端和写端,读端,写端,对应两个文件描述符 fd[0]、fd[1]。
  2. 读端操作的时候写端不能操作,写端操作的时候读端不能操作。
  3. 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
  4. 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
  5. 本质:内核缓冲区。伪文件-不占用磁盘空间
  6. 父子进程退出后管道消失
  7. 管道中的数据读取完毕就消失,存储在缓存中。数据只能读取一次,不能重复读取

函数原型:

1 #include <unistd.h>
2 int pipe(int fd[2]);    // 返回值:若成功返回0,失败返回-1

fd ‐ 传出参数:
           fd[0] ‐ 读端
           fd[1] ‐ 写端
 返回值:
             0:成功
            ‐1:创建失败
  • 父进程与子进程中的管道,在调用pipe的进程接着调用fork

Linux系统编程——进程间的通信_第1张图片

管道的读写

读操作

  • 有数据:read(fd[1]) 正常读,返回读出的字节数
  • 无数据:写端被全部关闭:read 返回0,相当于读文件到了尾部。没有全部关闭:read 阻塞

写操作

  • 读端全部关闭:管道破裂,进程被终止。内核给当前进程发送信号SIGPIPE-13,默认处理动作
  • 读端没全部关闭:缓冲区写满了:write阻塞。缓冲区没满:write继续写,直到写满,阻塞

父子进程使用管道通信实现 ps aux| grep “bash”

  • 调用 pipe 的进程接着调用 fork,这样就创建了父进程和子进程直接的 IPC 通道。
#include 
#include 
#include 
 
int main()
{
	int ret;
	int fd[2];
	ret = pipe(fd); // 创建匿名管道
	if (ret == -1)
	{
		printf("create pipe failed!~\n");
		exit(1);
	}
 
	pid_t pid = fork(); // 创建父子进程
	if (pid == -1)
	{
		printf("fork failed!~");
		exit(1);
	}
	// 父进程 执行 ps aux
	if (pid > 0)
	{
		close(fd[0]);				// 操作写端要先关闭读端
		dup2(fd[1], STDOUT_FILENO); // 把原本输出在 STDOUT 的内容重定向至管道写端
		execlp("ps", "ps", "aux", NULL);//原本输出在 STDOUT 的内容重定向至管道写端
		perror("execlp");
		exit(1);
	}
	// 子进程 执行 grep "bash"
	else if (pid == 0)
	{
		close(fd[1]);			   // 操作读端要先关闭写端
		dup2(fd[0], STDIN_FILENO); // 把原本输出在 STDIN 的内容重定向至管道读端
		execlp("grep", "grep", "bash", "--color=auto", NULL);//原本输出在 STDIN 的内容重定向至管道读端
		perror("execlp");
	}
 
	close(fd[0]);
	close(fd[1]);
	return 0;
}

命名管道 FIFO

特点:

  1. 除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信
  2. 半双工通信方式
  3. FIFO可以在无关的进程之间交换数据,与无名管道不同。
  4. FIFO有路径名与之相关联,它以一种特殊设备文件形式(p类型)存在于文件系统中。 FIFO 的通信方式类似于在进程中使用文件来传输数据,只不过 FIFO 类型文件同时具有管道的特性。在数据读出时,FIFO 管道中同时清除数据,而且“先进先出”。
  5. 数据存在内核中有一个对应的缓冲区
  6. 也是一个伪文件,在磁盘大小永久为0

函数原型:

1 #include <sys/stat.h>
2 // 返回值:成功返回0,出错返回-1
3 int mkfifo(const char *pathname, mode_t mode);

其中的 mode 参数与open函数中的 mode 相同。
一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。

mode:

  • O_RDONLY 只读打开
  • O_WRONLY 只写打开
  • O_RDWR 可读可写打开

返回值:

  • 创建成功返回0,创建失败返回-1。

命名管道的创建

  #include
  #include
  #include
  #include
  
  int main()
  {   // 创建FIFO管道
     if(mkfifo("./file", 0666) == -1 && errno!=EEXIST){
    	printf("mkfifo failuer\n");
        perror("why");
	 } 
      return 0;
}

当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:

  • 若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。【只读只写都会阻塞,只有当另一个进程读写该FIFO文件时才会打通】【管道的创建就是为了交换数据,要阻塞防止一直读取数据】
  • 若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。

fifo 案例实现两进程通信

mkfifo.c

#include 
#include 
#include 
#include 
#include 
 
int main()
{
	int ret;
	int fd;
	int nread;
	char readBuf[50] = {0};
	ret = mkfifo("/home/fangjiarong/test/myfifo", 0777); // 创建 fifo,以绝对路径创建
	if (ret == -1)
	{
		perror("mkfifo");
		return -1;
	}
	else
	{
		printf("creat fifo succeed!~\n");
	}
 
	fd = open("./myfifo", O_RDONLY); // 打开 fifo,要与另一个进程相同的路径
	if (fd < 0)
	{
		perror("fd");
		return -1;
	}
	else
	{
		printf("open fifo succeed!~\n");
	}
	nread = read(fd, readBuf, sizeof(readBuf)); //只读程序会阻塞,等待只写程序完成
	printf("read %d byte from fifo %s:\n", nread, readBuf);
	close(fd);
	return 0;
}

write.c

#include 
#include 
#include 
#include 
#include 
#include 
 
int main()
{
	char *str = "Hello World!~";
	int fd;
	fd = open("./myfifo", O_WRONLY);// 打开 fifo,要与另一个进程相同的路径
	if (fd < 0)
	{
		perror("fd");
		return -1;
	}
	else
	{
		printf("open fifo succeed!~\n");
	}
 
	write(fd, str, strlen(str));
	close(fd);
	return 0;
}

消息队列

  • 消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
  1. 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
  2. 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
  3. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取
  4. 消息队列某个进程读取完数据之后不会清除数据,除非手动调用清除队列,这点与管道不一样

消息队列结构体:

 // 消息结构体
 struct msg_form {
     long mtype;//消息类型
     char mtext[256];//消息zwe
};

函数原型:

#include 
int msgget(key_t key, int flag);
// 创建或打开消息队列

参数:

  • key:和消息队列关联的 key 值
  • flag:是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。flg 可以与 IPC_CREAT 做或操作,表示当 key 所命名的消息队列不存在时创建一个消息队列,如果 key 所命名的消息队列存在时,IPC_CREAT 标志会被忽略,而只返回一个标识符

返回值:

  • 成功返回队列ID,
  • 失败返回-1

在以下两种情况下,msgget将创建一个新的消息队列:

  • 如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。(消息队列的权限也写在flag位置)
  • key参数为IPC_PRIVATE
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 添加消息

参数:

  • msgid:消息队列的 ID
  • ptr:指向消息结构体struct msg_form的指针
  • size:发送的消息正文你的字节数
  • flag:
    • IPC_NOWAIT: 消息没有发送完成函数也会立即返回
    • 0: 直到发送完成函数才返回,flag取0表示读不到会阻塞

返回值:

  • 成功返回0,
  • 失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
//从一个消息队列中获取消息

参数:

  • msgid:消息队列的 ID
  • msgp:要接收消息的缓冲区
  • size:要接收的消息的字节数
  • type:type表示消息的类型值
    • type == 0,返回队列中的第一个消息;
    • type > 0,返回队列中消息类型为 type 的第一个消息;
    • type < 0,返回队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。
    • 可以看出,type值非 0 时用于以非先进先出次序读消息。也可以把 type 看做优先级的权值。
  • flag:
    • 0:若无消息函数一直阻塞
    • IPC_NOWAIT:若没有消息,进程会立即返回 ENOMSG。

返回值:

  • 成功:接收到的消息i长度
  • 出错:‐1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
// 控制消息队列

参数:

  • msqid:消息队列的队列ID

  • cmd:cmd常用参数为IPC_RMID。表示移除链表

    • IPC_STAT:把 msgid_ds 结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖 msgid_ds 的值。
    • IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为 msgid_ds 结构中给出的值
    • IPC_RMID:删除消息队列
  • buf:是指向 msgid_ds 结构的指针,它指向消息队列模式和访问权限的结构,一般第三个参数写null

返回值:

  • 成功:0
  • 失败:‐1

fork

  • 系统建立IPC通讯 (消息队列、信号量和共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到。
  • 函数原型:
key_t ftok( const char * fname, int id )
//key = ftok(".", 1); 这样就是将fname设为当前目录。
  • 参数:
    • fname就时你指定的文件名(该文件必须是存在而且可以访问的)。一般使用当前目录下
    • id是子序号, 虽然为int,但是只有8个比特被使用(0‐255)。
  • 返回值:
    • 当成功执行的时候,一个 key_t 值将会被返回,否则 ‐1 被返回

在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。
如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。

查询文件索引节点号的方法是:(当前路径下的索引号)

ls -i

显示当前消息队列信息

ipcs -q

消息队列实现全双工通信

实现流程:

  • 服务端创建父子进程,父进程负责发送,子进程负责接收
  • 客户端也创建父子进程,父进程负责发送,子进程负责接收
  • 客户端和服务端所创建的队列ID是一样的才能相互通信
  • 需要设置消息队列结构体里面的type参数,在调用msgrcv的时候需要传递的形参包含type,读取信息才准确

msg_service.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
struct msgbuf // 消息队列结构体
{
	long mtype;
	char mtext[128];

};
 
int main()
{
	struct msgbuf sendbuf, readbuf;
	int msgId;
	key_t key;
	int readret;
	pid_t pid;
	key = ftok("a.c", 1);				   // 获取 key 值
	msgId = msgget(key, IPC_CREAT | 0755); // 创建消息队列
	if (msgId == -1)
	{
		printf("create message queue failed!~\n");
		perror("msgget");
		return -1;
	}
	printf("create msgage queue succeeded!~ msgId = %d\n", msgId);
	system("ipcs -q"); // 显示当前消息队列信息
 
	// init msgbuf
	sendbuf.mtype = 100;
	pid = fork();
 
	// parent process write 100
	if (pid > 0)
	{
		while (1)
		{
			memset(sendbuf.mtext, 0, 128);
			printf("please input to message queue:\n");
			fgets(sendbuf.mtext, 128, stdin);
 
			// send message to message queue
			msgsnd(msgId, (void *)&sendbuf, strlen(sendbuf.mtext), 0);
		}
	}
 
	// child process read 200
	if (pid == 0)
	{
		while (1)
		{
			memset(readbuf.mtext, 0, 128);
			msgrcv(msgId, (void *)&readbuf, 128, 200, 0);
			printf("datas from client: %s\n", readbuf.mtext);
		}
	}
 
	return 0;
}

msg_client.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
struct msgbuf // 消息队列结构体
{
	long mtype;
	char mtext[128];

};
 
int main()
{
	struct msgbuf sendbuf, readbuf;
	int msgId;
	key_t key;
	int readret;
	pid_t pid;
	key = ftok("a.c", 1);				   // 获取 key 值
	msgId = msgget(key, IPC_CREAT | 0755); // 创建消息队列
	if (msgId == -1)
	{
		printf("create message queue failed!~\n");
		perror("msgget");
		return -1;
	}
	printf("create msgage queue succeeded!~ msgId = %d\n", msgId);
	system("ipcs -q"); // 显示当前消息队列信息
 
	// init msgbuf
	sendbuf.mtype = 200;
	pid = fork();
 
	// child process write 200
	if (pid == 0)
	{
		while (1)
		{
			memset(sendbuf.mtext, 0, 128);
			printf("please input to message queue:\n");
			fgets(sendbuf.mtext, 128, stdin);
 
			// send message to message queue
			msgsnd(msgId, (void *)&sendbuf, strlen(sendbuf.mtext), 0);
		}
	}
 
	// parent process read 100
	if (pid > 0)
	{
		while (1)
		{
			memset(readbuf.mtext, 0, 128);
			msgrcv(msgId, (void *)&readbuf, 128, 100, 0);
			printf("datas from service: %s\n", readbuf.mtext);
		}
	}
 
	return 0;
}

共享内存

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

特点:

  1. 共享内存是最快的一种 IPC,因为多个进程是直接对一段物理内存进行存取。该共享内存空间是映射到多个进程里面的
  2. 因为多个进程可以同时操作,所以需要进行同步。
  3. 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。通过信号量来控制两个进程只有一个进程能写一个进程能读,避免同时写入的操作
  4. 共享内存创建之后,一直存在于内核中,读取后,内容仍然在共享内存中。直到被删除或系统关闭

函数原型:

#include 
int shmget(key_t key, size_t size, int flag);
// 创建或获取一个共享内存:
  • 参数:
    • key:IPC_PRIVATE 或 ftok 的返回值
    • size:共享内存区大小,以1024为一个单位
    • flg:同 open 函数的权限位,也可以用8进制表示法f,可以包含IPC_CREAT标志位
  • 返回值:
    • 成功:共享内存段标识符‐‐‐ID‐‐‐文件描述符
    • 出错:‐1
// 连接共享内存到当前进程的地址空间
void *shmat(int shm_id, const void *addr, int flag);
  • 参数:
    • shm_id:ID号
    • addr:映射到的地址,写NULL 或0为内核自动分配内存地址
    • flg:
      • SHM_RDONLY 共享内存只读
      • 默认是 0,表示共享内存可读写
  • 返回值:
    • 成功:映射后的地址,也就是指向共享内存的指针
    • 失败:-1
// 断开与共享内存的连接
int shmdt(void *addr);
  • 参数:
    • shmid:要操作的共享内存标识符
  • 返回值:
    • 成功:0
    • 出错:‐1
// 控制共享内存的相关信息
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
  • 参数:
    • shm_id:要操作的共享内存标识符
    • cmd :
      • IPC_STAT (获取对象属性)‐‐‐ 实现了命令ipcs ‐m
      • IPC_SET (设置对象属性)
      • IPC_RMID (删除对象) ‐‐‐实现了命令ipcrm ‐m
    • buf :指定 IPC_STAT/IPC_SET 时用以保存/设置属性,一般写0
  • 返回值:
    • 成功:0
    • 出错:‐1

查看系统中的共享内存

ipcs -m

查询结果:
在这里插入图片描述
参数说明:

  • nattch表示连接共享内存的进程数目

删除系统中的共享内存的命令

ipcrm -m shmid
//写入shmid号

共享内存实现两进程通信

实现步骤

  1. 创建共享内存
  2. 将共享内存映射到

shmWrite.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
int main()
{
	int shmId;
	int key;
	char *p;
	key = ftok("a.c", 1); // 生成 key 值
	if (key < 0)
	{
		perror("ftok");
		return -1;
	}
	printf("ftok succeed!~ key = %x\n", key);
	shmId = shmget(key, 128, IPC_CREAT | 0777); // 创建共享内存
	if (shmId < 0)
	{
		perror("share memory:");
		return -1;
	}
	printf("create share memory succeeded!~ shmId = %d\n", shmId);
	system("ipcs -m");
	p = (char *)shmat(shmId, NULL, 0); // 共享内存映射,配置可读可写的方式由系统自动映射
	if (p == NULL)
	{
		perror("share memory function");
		return -1;
	}
	memset(p, 0, 128);
	// write to share memory
	while (1)
	{
		fgets(p, 128, stdin);
	}
 
	return 0;
}

shmRead.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
int main()
{
	int shmId;
	int key;
	char *p;
	key = ftok("a.c", 1);
	if (key < 0)
	{
		perror("ftok");
		return -1;
	}
	printf("ftok succeed!~ key = %x\n", key);
	shmId = shmget(key, 128, 0);
	if (shmId < 0)
	{
		perror("share memory:");
		return -1;
	}
	printf("create share memory succeeded!~ shmId = %d\n", shmId);
	system("ipcs -m");
	p = (char *)shmat(shmId, NULL, 0);
	if (p == NULL)
	{
		perror("share memory function");
		return -1;
	}
	while (1)
	{
		sleep(5);
		printf("share memory data = %s\n", p);
		memset(p, 0, 128);
	}
 
	// shmdt(p);
	return 0;
}

信号

信号的概述

Linux 信号(signal)

信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生主要作为进程间以及同一进程不同线程之间的同步手段。通过给进程发送相应信号,该进程接收到此信号后运行信号处理函数或者作出默认或忽略回应

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

  • ctrl+c是shell指令信号,往当前进程发送停止信号
  • ctrl+c对应的信号名称为SIGINT

信号的名字和编号:

  • 每个信号都有一个名字和编号,这些名字都以“SIG”开头

  • 信号定义在signal.h头文件中,信号名都定义为正整数。

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

1) SIGHUP	    2) SIGINT	  3) SIGQUIT	  4) SIGILL	     5) SIGTRAP
 6) SIGABRT	    7) SIGBUS	  8) SIGFPE	      9) SIGKILL	10) SIGUSR1
11) SIGSEGV	   12) SIGUSR2	 13) SIGPIPE	 14) SIGALRM	15) SIGTERM
16) SIGSTKFLT  17) SIGCHLD	 18) SIGCONT	 19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	   22) SIGTTOU	 23) SIGURG	     24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM  27) SIGPROF	 28) SIGWINCH    29) SIGIO	    30) SIGPWR
31) SIGSYS	   34) SIGRTMIN
信号名 含义 默认操作
SIGHUP 该信号在用户终端连接(正常或非正常)结束时发出,通常是在终端的控制进程结束时,通知同一会话内的各个作业与控制终端不再关联。 终止
SIGINT 该信号在用户键入INTR字符(通常是Ctrl-C)时发出,终端驱动程序发送此信号并送到前台进程中的每一个进程。 终止
SIGQUIT 该信号和SIGINT类似,但由QUIT字符(通常是Ctrl-)来控制。 终止
SIGKILL 该信号在一个进程企图执行一条非法指令时(可执行文件本身出现错误,或者试图执行数据段、堆栈溢出时)发出。 终止
SIGFPE 该信号在发生致命的算术运算错误时发出。这里不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术的错误。 终止
SIGKILL 该信号用来立即结束程序的运行,并且不能被阻塞、处理和忽略。 终止
SIGALRM 该信号当一个定时器到时的时候发出。 终止
SIGSTOP 该信号用于暂停一个进程,且不能被阻塞、处理或忽略。ctrl+z 暂停
SIGTSTP 该信号用于暂停交互进程,用户可键入SUSP字符(通常是Ctrl-Z)发出这个信号。 暂停
SIGCHLD 子进程改变状态时(exit函数),父进程会收到这个信号 忽略
SIGABORT 该信号用于结束进程 终止

信号通信的框架

  • 信号的发送(发送信号进程):kill、raise、alarm、sigqueue
  • 信号的处理(接收信号进程) :signal、sigaction
  • 信号的接收(接收信号进程) : pause()、 sleep、 while(1) // 进程要存在才能接受信号

信号的使用 :

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

对于信号来说,最大的意义不是为了杀死信号,而是实现一些异步通讯的手段 (即捕捉信号)

信号的处理:

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

信号发送函数

  1. 入门版:kill
  2. 高级版:sigqueue

高级版会携带参数,而不只是入门版的只关注函数

kill

#include
#include
int kill(pid_t pid, int sig);
  • 参数:
    • 函数传入值:pid
      • 正数:要接收信号的进程的进程号
      • 0:信号被发送到所有和pid进程在同一个进程组的进程
      • ‐1:信号发给所有的进程表中的进程(除了进程号最大的进程外)
    • sig:信号
  • 函数返回值:
    • 成功 0
    • 出错 ‐1

raise

#include
#include
函数原型:
int  raise(int sig);
  • 参数:
    • sig:信号
  • 函数返回值:
    • 成功 0
    • 出错 ‐1

发信号给自己 == kill(getpid(), sig)

alarm

函数原型

#include  
unsigned int  alarm(unsigned int seconds)
  • 参数:
    • seconds:指定秒数
  • 返回值:
    • 成功:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,
    • 否则返回0。
    • 出错:‐1

alarm 与 raise 函数的比较:

相同点:

  • 让内核发送信号给当前进程

不同点:

  • alarm 只会发送 SIGALARM 信号
  • alarm 会让内核定时一段时间之后发送信号, raise 会让内核立刻发信号

sigqueue 的函数原型

#include 
int sigqueue(pid_t pid, int sig, const union sigval value);
//联合体参数成员
union sigval {
   int   sival_int;//发送信号携带的参数为int
   void *sival_ptr; //发送信号携带的参数为指针
 };

int main(int argc, char** argv){
    int pid = atoi(argv[1]);
    int sig = atoi(argv[2]);
}

使用这个函数之前,必须要有几个操作需要完成:

  1. 使用 sigaction 函数安装信号处理程序时,制定了 SA_SIGINFO 的标志。
  2. sigaction 结构体中的 sa_sigaction 成员提供了信号捕捉函数。如果实现的时 sa_handler 成员,那么将无法获取额外携带的数据。

信号处理函数

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

功能:注册信号处理函数,当系统收到该信号时,系统会执行信号处理函数

signal 的函数原型

#include 
typedef void (*sighandler_t)(int);
//sighandler_t表示函数指针,该指针指向函数,函数的返回值为void
//参数是 int 类型,显然也是信号产生的类型,方便使用一个函数来处理多个信号
// typedef主要用于变量类型的定义别名


sighandler_t signal(int signum, sighandler_t handler);
//signum 显然是信号的编号,handler 是中断函数的指针
//返回值是sighandler_t,即函数指针
//第二个参数的数据类型是sighandler_t,表示函数指针,此时传递函数的地址值

//此时signnum参数收集的是信号名称对应的信号编号
void handler(int signum){
    printf("get signum=%d\n",signum);
}

signal第二个参数也可以是宏:

  • SIG_IGN:忽略信号的执行
  • SIG_DFL:采用系统默认方式处理信号
  • 自定义的信号处理函数指针

利用信号处理函数实现一个进程kill另一个进程

int main(int argc, char** argv){
    int pid = atoi(argv[1]);//将字符串转化为整型
    int signum = atoi(argv[2]);
    char cmd[128]={0};//command
    sprintf(cmd,"kill -%d %d",signum,pid);
    //sprintf指的是字符串格式化命令,函数声明为 int sprintf(char *string, char *format [,argument,...]);,
    //主要功能是把格式化的数据写入某个字符串中,即发送格式化输出到 string 所指向的字符串。sprintf 是个变参函数
    
    system(cmd);//该函数表示执行一条指令,该指令以字符串形式作为参数
    return 0;
    
}

sigaction 的函数原型

#include 
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//第一个参数signum应该就是注册的信号的编号;
//第二个参数act如果不为空说明需要对该信号有新的配置;函数配置写在第二个参数里面;结构体指针,将结构体取地址值
//第三个参数oldact如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。可以为NULL

//需要配置的结构体中的成员
struct sigaction {
   void (*sa_handler)(int); 
    //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
   void (*sa_sigaction)(int, siginfo_t *, void *); 
    //信号处理函数,能够接受额外数据和sigqueue配合使用
    //第一个参数信号的编号名称
    //第二个参数siginfo_t *是结构体指针,主要适用于记录接收信号的一些相关信息。例如si_int 或si_value.sival_int。要用->指向成员
    //第三个参数void* 是接收到信号所携带的额外数据;指针为空的时候没有数据,反之则有,有数据的时候才操作siginfo结构体
   sigset_t sa_mask;
    //阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。
   int sa_flags;
    //影响信号的行为SA_SIGINFO表示能够接受数据
 };
//回调函数句柄sa_handler、sa_sigaction只能任选其一


//signinfo_t结构体
 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;
 };

理解:

struct sigaction {
	void (*sa_sigaction)(int, siginfo_t *, void *); 
    //变量类型为void (*) (int ,siginfo_t*,void*),变量名为sa_sigaction
	int sa_flags;//变量类型为int,变量名为sa_flags
}

利用信号实现进程间通信

执行流程:

  • fork 后父子进程各自运行,子进程等待三秒,父进程在 while(1) 打印信息。三秒后子进程给父进程发送 10 号与 17 号信号。父进程通过 signal 函数接收到子进程发过来的信号,去执行信号服务函数 sig_usr1 与 sig_chld 。
#include 
#include 
#include 
#include 
#include 
#include 
 
void sig_usr1(int signum) // 10号信号处理函数
{
	int i = 0;
	while (i < 5)
	{
		i++;
		printf("receive signum = %d, i = %d\n", signum, i);
		sleep(1);
	}
}
 
void sig_chld(int signum) // 17号信号处理函数
{
	printf("receive signum = %d \n", signum);
	printf("回收子进程\n");
	wait(NULL); // 回收子进程,防止其变成僵尸进程
}
 
int main()
{
	pid_t pid;
	pid = fork();
	if (pid > 0) // 父进程
	{
		int i = 0;
		signal(10, sig_usr1); // 10号 SIGUSR1 信号
		signal(17, sig_chld); // 17号 SIGCHLD 信号
		while (1)
		{
			i++;
			printf("parent process i = %d\n", i);
			sleep(1);
		}
	}
 
	else if (pid == 0) // 子进程
	{
		sleep(3);
		kill(getppid(), 10);
		exit(0); // 相当于给父进程发送17号信号 kill(getppid, 17)
	}
 
	return 0;
}

信号量

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

  • 它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

  • 临界资源:多个进程共享资源,然而资源一次只能给一个进程使用则称为临界资源,例如输入机,打印机等

  • 解决了共享内存中多个进程同时操作共享内存的问题,当一个进程在操作时另一个进程不能使用该共享内存,即信号量用于管理临界资源,

  • 将信号量类比于钥匙,房间类比于临界资源,p操作拿钥匙,v操作放回钥匙

  • 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

  • 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。

  • 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

  • 支持信号量组。

#include 
#include 
#include 
            
// 创建或获取一个信号量组或获取一个已经存在的信号量的键值。
int semget(key_t key, int num_sems, int sem_flags);
  • 函数参数:
    • key:和信号量集关联的key值
    • num_sems: 信号量集中包含的信号量数目
    • sem_flags:信号量集的访问权限,可以加IPC_CREAT
  • 函数返回值:
    • 成功:信号灯集ID
    • 出错:‐1
#include 
#include 
#include 
// 初始化信号量
int semctl(int semid, int sem_num, int cmd, ...);

// 联合体,用于semctl初始化
union semun
{
    int              val; /*for SETVAL*/
    struct semid_ds *buf;
    unsigned short  *array;
}
  • 函数参数:
    • semid:信号灯集ID
    • sem_num: 要修改的信号灯编号,第几个信号量
    • cmd :
      • GETVAL:获取信号灯的值
      • SETVAL:设置信号灯的值(一般选SETVAL;表示要设置信号量的值,设置为semun联合体里面的val值)
      • IPC_RMID:从系统中删除信号灯集合
    • 第四个参数取决于第三个参数,第四个参数是union semun联合体,写入联合体变量名,在main函数中配置联合体参数val为1,信号量的值对应有无钥匙
  • 函数返回值:
    • 成功:0
    • 出错:‐1
#include
//pv操作函数都是调用semop函数
// 对信号量组进行操作,改变信号量的值:也就是使用资源还是释放资源使用权
int semop(int semid, struct sembuf semoparray[], size_t numops);  

//struct sembuf结构体
struct sembuf
{
    short sem_num; // 信号量组中对应的序号,0~sem_nums-1
    short sem_op;  // 信号量值在一次操作中的改变量,p操作时-1 v操作时-1
    short sem_flg; // IPC_NOWAIT, SEM_UNDO(当进程终止的时候自动结束对钥匙的操作),0阻塞,1非阻塞
}

//定义结构体变量
struct sembuf sops;
sops.sem_num = 
sops.sem_op = 
sops.sem_flg = 

//定义结构体数组
struct sembuf sops[2];
sops[0].sem_num = 
sops[0].sem_op = 
sops[0].sem_flg = 

参数说明:

  • 第一个参数是信号量的标识码。也就是semget()的返回值
  • 第二个参数数组,如果信号量集里面只有一个信号可以不写数组,只写结构体变量
  • 第三个参数是第二个参数(数组)里面成员的个数

返回值

  • 成功返回0,
  • 失败返回-1

代码实例:

#include 
#include 
#include 
#include 
#include 
#include 
 
#define SEM_READ 0
#define SEM_WRITE 1
 
union semun
{
	int val;
};
 
void P_operation(int index, int semId)
{
	struct sembuf sop;
	sop.sem_num = index; //信号灯编号
	sop.sem_op = -1;	 // P 操作
	sop.sem_flg = 0;	 // 阻塞
	semop(semId, &sop, 1);
}
 
void V_operation(int index, int semId)
{
	struct sembuf sop;
	sop.sem_num = index; //信号灯编号
	sop.sem_op = 1;		 // V 操作
	sop.sem_flg = 0;	 // 阻塞
	semop(semId, &sop, 1);
}
 
int main()
{
	key_t key;
	key = ftok("b.c", 123); // 获取 key 值
	pid_t pid;
	int semId; // 信号灯 ID
	int shmId; // 共享内存 ID
 
	char *shamaddr;
 
	semId = semget(key, 2, IPC_CREAT | 0755); // 创建信号量
	if (semId < 0)
	{
		perror("semget error");
		return -1;
	}
	shmId = shmget(key, 128, IPC_CREAT | 0755); // 创建共享内存
	if (shmId < 0)
	{
		perror("shmget error");
		return -1;
	}
 
	// Init semaphore
	union semun myun;
	// Init semaphore read
	myun.val = 0;
	semctl(semId, SEM_READ, SETVAL, myun); // 对 SEM_READ 信号量设置初始值
	// Init semaphore write
	myun.val = 1;
	semctl(semId, SEM_WRITE, SETVAL, myun); // 对 SEM_WRITE 信号量设置初始值
 
	pid = fork();
 
	// child process
	if (pid == 0)
	{
		while (1)
		{
			shamaddr = (char *)shmat(shmId, NULL, 0);		// 共享内存映射
			P_operation(SEM_READ, semId);					// P 操作
			printf(" get share memory is: %s\n", shamaddr); // 操作对共享资源
			V_operation(SEM_WRITE, semId);					// V 操作
		}
	}
 
	// parent process
	else if (pid > 0)
	{
		while (1)
		{
			shamaddr = (char *)shmat(shmId, NULL, 0);
			P_operation(SEM_WRITE, semId);
			printf("please input to share memory\n");
			fgets(shamaddr, 32, stdin);
			V_operation(SEM_READ, semId);
		}
	}
 
	return 0;
}

你可能感兴趣的:(Linux,linux,运维,服务器)