文章目录
- 11. 进程间通信
- 11.1 管道
- 11.1.0 |
- 11.1.1 匿名管道
- 11.1.2 命名管道
- 11.1.3 用匿名管道形成进程池
- 11.2 system V共享内存
- 11.2.1 system V函数
- 11.2.2 system 命令
- 11.3 system V消息队列
- 11.4 system V 信号量
- 12. 进程信号
- 12.1 前台进程和后台进程
- 12.1.1 jobs
- 12.1.2 fg & bg
- 12.2 硬件中断和软件中断
- 12.3 信号
- 12.3.1 kill -L
- 12.3.2 信号的常见处理方式
- 12.3.3 信号的发送
- 12.3.4 signal
- 12.4 信号的产生
- 12.4.1 通过键盘进行信号产生
- 12.4.1.1 core dump
- 12.4.1.2 SIGINT 和 SIGQUIT
- 12.4.2 通过系统调用指令
- 12.4.3 异常
- 12.4.4 软件条件
- 12.5信号的保存
- 12.5.1 信号的相关概念
- 12.5.2 信号在内核中的表示
- 12.5.3 sigset_t
- 12.5.4 信号集操作函数
- 12.5.5 内核如何实现信号捕捉
- 12.5.6 用户态和内核态
- 12.5.7 sigaction
- 12.5.8 SIGCHILD
- 12.6 可重入函数
- 12.7 volatile
11. 进程间通信
进程间通信(Inter-Process Communication,IPC)是指不同进程之间进行数据交换和同步的机制。在多任务操作系统中,多个进程可能同时存在并运行,它们之间可能需要相互通信以完成某些任务。IPC 提供了一种机制,允许进程之间传递信息、共享数据,并在必要时进行同步。
为了让两个进程之间进行通信,首先需要让它们看到同一份资源。
以下是几种常见的进程间通信方式:
- 管道(Pipes):
- 匿名管道用于具有亲缘关系的进程间的通信,如父进程与子进程之间。它是单向的,数据只能单向流动。Linux/Unix系统中可以通过
pipe()
系统调用创建管道。
- 匿名管道用于具有亲缘关系的进程间的通信,如父进程与子进程之间。它是单向的,数据只能单向流动。Linux/Unix系统中可以通过
- 命名管道(Named Pipes):
- 命名管道允许任何进程之间进行通信,即使它们没有亲缘关系。命名管道是一种特殊的文件系统对象。在Linux/Unix中,使用
mkfifo
命令可以创建命名管道。
- 命名管道允许任何进程之间进行通信,即使它们没有亲缘关系。命名管道是一种特殊的文件系统对象。在Linux/Unix中,使用
- 消息队列(Message Queues):
- 消息队列允许进程通过向队列发送和接收消息来通信。它们是在内核中维护的消息链表,每个消息都有一个类型以及一个用于标识消息的整数标识符。在Linux/Unix系统中,可以使用
msgget()
、msgsnd()
和msgrcv()
等函数来操作消息队列。
- 消息队列允许进程通过向队列发送和接收消息来通信。它们是在内核中维护的消息链表,每个消息都有一个类型以及一个用于标识消息的整数标识符。在Linux/Unix系统中,可以使用
- 共享内存(Shared Memory):
- 共享内存允许两个或多个进程共享同一块内存区域。这对于需要高性能数据交换的进程非常有用,因为它避免了数据的复制。在Linux/Unix中,可以使用
shmget()
、shmat()
和shmdt()
等函数来创建和操作共享内存区域。
- 共享内存允许两个或多个进程共享同一块内存区域。这对于需要高性能数据交换的进程非常有用,因为它避免了数据的复制。在Linux/Unix中,可以使用
- 信号量(Semaphores):
- 信号量是一个计数器,用于控制多个进程对共享资源的访问。它可以用于解决竞争条件和同步问题。在Linux/Unix中,可以使用
semget()
、semop()
和semctl()
等函数来创建和操作信号量。
- 信号量是一个计数器,用于控制多个进程对共享资源的访问。它可以用于解决竞争条件和同步问题。在Linux/Unix中,可以使用
进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信的分类
- 管道(Pipes):
- 匿名管道(pipe)和命名管道(named pipe)是两种常见的管道形式,用于在具有亲缘关系或非亲缘关系的进程之间进行通信。管道是单向的,只能支持单向数据流动。
- System V IPC:
- System V IPC 是一套用于进程间通信的标准,提供了三种主要的通信机制:消息队列、共享内存和信号量。这些机制在早期Unix系统上广泛使用,但在现代系统中可能已经被POSIX IPC替代。
- System V 消息队列:允许进程通过消息队列发送和接收消息,消息队列在内核中维护,进程通过系统调用进行操作。
- System V 共享内存:允许多个进程访问同一块共享内存区域,从而实现快速的数据交换。进程通过获取共享内存的标识符来访问共享内存区域。
- System V 信号量:信号量用于控制多个进程对共享资源的访问。它可以用于实现互斥和同步操作,以防止竞争条件的发生。
- System V IPC 是一套用于进程间通信的标准,提供了三种主要的通信机制:消息队列、共享内存和信号量。这些机制在早期Unix系统上广泛使用,但在现代系统中可能已经被POSIX IPC替代。
- POSIX IPC:
- POSIX IPC 是一套与POSIX标准兼容的进程间通信机制,与System V IPC相比,它更加简单、灵活,并且更容易使用。它包括消息队列、共享内存、信号量、互斥量、条件变量和读写锁等机制。
- 消息队列:与System V消息队列类似,允许进程通过消息队列发送和接收消息。
- 共享内存:与System V共享内存类似,允许多个进程访问同一块共享内存区域。
- 信号量:与System V信号量类似,用于控制多个进程对共享资源的访问。
- 互斥量(Mutex):用于实现互斥访问共享资源,只能被一个线程持有。
- 条件变量(Condition Variable):用于线程间的同步,当满足特定条件时,允许线程挂起或唤醒。
- 读写锁(Read-Write Lock):用于控制对共享资源的读写访问,允许多个线程同时读取,但只允许一个线程进行写入。
- POSIX IPC 是一套与POSIX标准兼容的进程间通信机制,与System V IPC相比,它更加简单、灵活,并且更容易使用。它包括消息队列、共享内存、信号量、互斥量、条件变量和读写锁等机制。
进程间通信的本质:让不同进程先看到同一份资源
11.1 管道
管道(Pipe)是一种在Unix/Linux系统中用于进程间通信的机制,通常用于具有亲缘关系的父子进程之间或者在同一个用户空间中的进程之间。管道是一种单向通信机制,数据只能在一个方向上流动。
管道可以分为两种类型:
- 匿名管道(Anonymous Pipes):
- 匿名管道是最简单的管道形式,只能用于具有亲缘关系的进程间通信,一般是父进程和子进程之间。匿名管道在创建时不需要指定名称,而是通过调用
pipe()
系统调用来创建,返回两个文件描述符,一个用于读取,一个用于写入。数据被写入到一个文件描述符后,可以通过另一个文件描述符进行读取。
- 匿名管道是最简单的管道形式,只能用于具有亲缘关系的进程间通信,一般是父进程和子进程之间。匿名管道在创建时不需要指定名称,而是通过调用
- 命名管道(Named Pipes,也称为FIFO):
- 命名管道允许任何进程间通信,即使它们没有亲缘关系。命名管道是一种特殊的文件系统对象,它有一个路径名,并且可以像文件一样被打开、读取和写入。命名管道可以通过
mkfifo
命令或者mkfifo()
系统调用创建。命名管道与匿名管道相似,但不同的是,它可以在文件系统中存在,并且可以通过文件系统进行访问。
- 命名管道允许任何进程间通信,即使它们没有亲缘关系。命名管道是一种特殊的文件系统对象,它有一个路径名,并且可以像文件一样被打开、读取和写入。命名管道可以通过
管道的特点包括:
- 单向性:管道是单向的,数据只能在一个方向上流动。
- 阻塞:当管道写满或者读空时,写入操作或读取操作可能会被阻塞,直到有数据可用或者有空间可写入。
- 安全性:由于管道的数据流只能在一个方向上进行,因此不存在读写同时发生的情况,从而避免了竞争条件。
管道通常用于父子进程之间或者一些需要简单数据交换的场景,但由于其单向性和阻塞特性,有时候可能会受到一些限制。
11.1.0 |
在Unix/Linux系统中,管道(Pipe)通常用竖线符号 |
表示。当我们在命令行中使用 |
符号时,实际上是在创建一个管道,将一个命令的输出作为另一个命令的输入。这种使用管道符号的方式称为管道命令(Pipeline)。
例如,如果我们想要将一个命令的输出传递给另一个命令进行处理,可以使用管道符号将它们连接起来。例如:
command1 | command2
这样,command1
的输出将作为 command2
的输入。这种方式实际上就是在使用匿名管道,在不同的进程之间进行通信和数据传递。
示例:
cat myfile.txt | wc -l
11.1.1 匿名管道
匿名管道(Anonymous Pipes)是一种在Unix/Linux系统中用于进程间通信的机制,通常用于具有亲缘关系的父子进程之间。它是一种单向通信机制,只能支持单向数据流动。
匿名管道具有以下特点:
- 单向性:匿名管道是单向的,数据只能在一个方向上流动。一般来说,管道是从一个进程的输出端流向另一个进程的输入端。
- 创建和销毁:匿名管道在创建时不需要指定名称,而是通过调用系统调用
pipe()
来创建。pipe()
系统调用会返回两个文件描述符,一个用于读取数据,一个用于写入数据。通常情况下,父进程调用pipe()
后会 fork 出子进程,父子进程通过这两个文件描述符进行通信。当进程结束时,操作系统会自动销毁管道。 - 简单性:匿名管道通常用于父子进程之间简单的数据传递,因此非常简单易用。但是由于其单向性,只能满足一些特定场景下的通信需求。
匿名管道的典型应用场景包括:
- 父进程与子进程之间的数据交换和通信。
- 进程管道(Process pipeline),其中多个进程依次处理同一数据流。
由于匿名管道的限制,它不适用于无亲缘关系的进程间通信,也无法在同一台机器上的不同用户之间进行通信。对于这些情况,可以使用命名管道(Named Pipes)或其他进程间通信机制来实现。
pipe()
pipe()
是一个Unix/Linux系统调用,用于创建匿名管道(Anonymous Pipe)。匿名管道是一种轻量级的进程间通信机制,通常用于具有亲缘关系的父子进程之间的通信。
pipe()
系统调用创建了一个管道,它是一个单向通道,其中一个文件描述符用于读取数据,另一个文件描述符用于写入数据。它的声明如下:
#include <unistd.h>
int pipe(int pipefd[2]);
其中 pipefd
是一个长度为 2 的整型数组,用于存放管道的读取端和写入端的文件描述符。pipefd[0]
表示管道的读取端,pipefd[1]
表示管道的写入端。
调用 pipe()
函数成功后,操作系统会创建一个管道,并分配两个文件描述符,一个用于读取管道数据,一个用于写入管道数据。通常情况下,pipe()
函数成功返回 0,失败返回 -1,并设置合适的错误码以指示失败原因。
匿名管道通常用于父子进程之间的通信,父进程可以使用 pipe()
创建管道后,再使用 fork()
创建子进程,然后通过管道进行数据交换。父进程可以关闭管道的读取端,子进程可以关闭管道的写入端,从而实现单向通信。
管道是一种有限的缓冲区,写入端写入数据时,如果管道已满,写操作可能会阻塞,直到有空间可用;读取端读取数据时,如果管道为空,读操作可能会阻塞,直到有数据可读取。
实例代码:
#include <iostream>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>#define MAX 1024using namespace std;// a. 管道的4种情况
// 1. 正常情况,如果管道没有数据了,读端必须等待,直到有数据为止(写端写入数据了)
// 2. 正常情况,如果管道被写满了,写端必须等待,直到有空间为止(读端读走数据)
// 3. 写端关闭,读端一直读取, 读端会读到read返回值为0, 表示读到文件结尾
// 4. 读端关闭,写端一直写入,OS会直接杀掉写端进程,通过想目标进程发送SIGPIPE(13)信号,终止目标进程 ---- 如何证明??
// b. 管道的5种特性
// 1. 匿名管道,可以允许具有血缘关系的进程之间进行进程间通信,常用与父子,仅限于此
// 2. 匿名管道,默认给读写端要提供同步机制
// 3. 面向字节流的
// 4. 管道的生命周期是随进程的
// 5. 管道是单向通信的,半双工通信的一种特殊情况int main()
{// 第1步,建立管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n == 0);(void)n; // 防止编译器告警,意料之中,用assert,意料之外,用ifcout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << endl;// 第2步,创建子进程pid_t id = fork();if (id < 0){perror("fork");return 1;}// 子写,父读// 第3步,父子关闭不需要的fd,形成单向通信的管道if (id == 0){// if(fork() > 0) exit(0); //// childclose(pipefd[0]);// w - 只向管道写入,没有打印int cnt = 0;while(true){// char c = 'a';// write(pipefd[1], &c, 1);// cnt++;// cout << "write ....: " << cnt << endl; // 我的机器的pipe空间大小是64KBchar message[MAX];snprintf(message, sizeof(message), "hello father, I am child, pid: %d, cnt: %d", getpid(), cnt);cnt++;write(pipefd[1], message, strlen(message));sleep(1);// if(cnt > 3) break;}cout << "child close w piont" << endl;// close(pipefd[1]);exit(0);}// 父进程close(pipefd[1]);// rchar buffer[MAX];while(true){// sleep(2000);ssize_t n = read(pipefd[0], buffer, sizeof(buffer)-1);if(n > 0){buffer[n] = 0; // '\0', 当做字符串cout << getpid() << ", " << "child say: " << buffer << " to me!" << endl;}else if(n == 0){cout << "child quit, me too !" << endl;break;}cout << "father return val(n): " << n << endl;sleep(1);break;}cout << "read point close"<< endl;close(pipefd[0]);sleep(5);int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid == id){cout << "wait success, child exit sig: " << (status&0x7F) << endl;}return 0;
}
用fork来共享管道原理
站在文件描述符角度-深度理解管道
11.1.2 命名管道
命名管道(Named Pipes),也称为 FIFO(First-In-First-Out),是一种在Unix/Linux系统中用于进程间通信的机制。与匿名管道不同,命名管道允许任何进程之间进行通信,即使它们没有亲缘关系。
命名管道是一种特殊的文件系统对象,它有一个路径名,并且可以像文件一样被打开、读取和写入。命名管道通过文件系统在磁盘上创建,因此可以在文件系统中存在,可以通过文件系统的路径进行访问。
使用命名管道的一般流程如下:
- 创建命名管道:可以使用
mkfifo
命令或者mkfifo()
系统调用来创建命名管道。例如:mkfifo /path/to/fifo
。 - 打开命名管道:进程可以像打开普通文件一样打开命名管道,使用文件路径来打开它。
- 进行读写操作:打开命名管道后,进程就可以向管道中写入数据,或者从管道中读取数据。管道的数据传输遵循先进先出(FIFO)的原则。
- 关闭命名管道:当通信完成后,进程应该关闭命名管道,释放资源。
命名管道的特点包括:
- 允许任意进程之间进行通信,即使它们没有亲缘关系。
- 类似于普通文件,可以通过文件系统的路径进行访问。
- 数据传输遵循先进先出(FIFO)的原则。
命名管道通常用于需要持续进行数据交换的进程之间的通信,例如在客户端和服务器之间传输数据,或者在不同的进程间进行数据传输和处理。
mkfifo()和mkfifo
mkfifo()
是一个Unix/Linux系统调用,用于创建命名管道(Named Pipe),也称为 FIFO(First-In-First-Out)。命名管道是一种进程间通信机制,允许任意进程之间进行通信,即使它们没有亲缘关系。
mkfifo()
的声明如下:
#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);
其中,pathname
是命名管道的路径名,mode
是创建命名管道的权限位。函数调用成功时返回 0,失败时返回 -1,并设置合适的错误码以指示失败原因。
mkfifo
命令是在Unix/Linux系统中用于创建命名管道的命令行工具。它的基本语法如下:
mkfifo [options] name...
其中,name
是要创建的命名管道的名称,可以指定多个名称。mkfifo
命令创建的命名管道将会在当前目录下创建。一旦创建成功,其他进程就可以通过文件系统路径名来访问这些命名管道。
命令行示例
终端1:while :; do echo “hello world”;sleep 1;done >fifo
终端2:cat < fifo
实例代码:
# makefile
.PHONY:all
all:server clientserver:server.ccg++ -o $@ $^ -std=c++11
client:client.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f server client fifo
//comm.h
#pragma once#define FILENAME "fifo"
//client.cc
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "comm.h"int main()
{int wfd = open(FILENAME, O_WRONLY);if (wfd < 0){std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;return 1;}std::cout << "open fifo success... write" << std::endl;std::string message;while (true){std::cout << "Please Enter# ";std::getline(std::cin, message);ssize_t s = write(wfd, message.c_str(), message.size());if (s < 0){std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;break;}}close(wfd);std::cout << "close fifo success..." << std::endl;return 0;
}
//server.cc
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "comm.h"bool MakeFifo()
{int n = mkfifo(FILENAME, 0666);if(n < 0){std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;return false;}std::cout << "mkfifo success... read" << std::endl;return true;
}int main()
{
Start:int rfd = open(FILENAME, O_RDONLY);if(rfd < 0){std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;if(MakeFifo()) goto Start;else return 1;}std::cout << "open fifo success..." << std::endl;char buffer[1024];while(true){ssize_t s = read(rfd, buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0;std::cout << "Client say# " << buffer << std::endl;}else if(s == 0){std::cout << "client quit, server quit too!" << std::endl;break;}}close(rfd);std::cout << "close fifo success..." << std::endl;return 0;
}
11.1.3 用匿名管道形成进程池
# makefile
processpool:ProcessPool.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f processpool
//ProcessPool.cc
#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"const int num = 5;
static int number = 1;class channel
{
public:channel(int fd, pid_t id) : ctrlfd(fd), workerid(id){name = "channel-" + std::to_string(number++);}public:int ctrlfd;pid_t workerid;std::string name;
};void Work()
{while (true){int code = 0;ssize_t n = read(0, &code, sizeof(code));if (n == sizeof(code)){if (!init.CheckSafe(code))continue;init.RunTask(code);}else if (n == 0){break;}else{// do nothing}}std::cout << "child quit" << std::endl;
}void PrintFd(const std::vector<int> &fds)
{std::cout << getpid() << " close fds: ";for(auto fd : fds){std::cout << fd << " ";}std::cout << std::endl;
}// 传参形式:
// 1. 输入参数:const &
// 2. 输出参数:*
// 3. 输入输出参数:&
void CreateChannels(std::vector<channel> *c)
{// bugstd::vector<int> old;for (int i = 0; i < num; i++){// 1. 定义并创建管道int pipefd[2];int n = pipe(pipefd);assert(n == 0);(void)n;// 2. 创建进程pid_t id = fork();assert(id != -1);// 3. 构建单向通信信道if (id == 0) // child{if(!old.empty()){for(auto fd : old){close(fd);}PrintFd(old);}close(pipefd[1]);dup2(pipefd[0], 0);Work();exit(0); // 会自动关闭自己打开的所有的fd}// fatherclose(pipefd[0]);c->push_back(channel(pipefd[1], id));old.push_back(pipefd[1]);// childid, pipefd[1]}
}void PrintDebug(const std::vector<channel> &c)
{for (const auto &channel : c){std::cout << channel.name << ", " << channel.ctrlfd << ", " << channel.workerid << std::endl;}
}void SendCommand(const std::vector<channel> &c, bool flag, int num = -1)
{int pos = 0;while (true){// 1. 选择任务int command = init.SelectTask();// 2. 选择信道(进程)const auto &channel = c[pos++];pos %= c.size();// debugstd::cout << "send command " << init.ToDesc(command) << "[" << command << "]"<< " in "<< channel.name << " worker is : " << channel.workerid << std::endl;// 3. 发送任务write(channel.ctrlfd, &command, sizeof(command));// 4. 判断是否要退出if (!flag){num--;if (num <= 0)break;}sleep(1);}std::cout << "SendCommand done..." << std::endl;
}
void ReleaseChannels(std::vector<channel> c)
{// version 2// int num = c.size() - 1;// for (; num >= 0; num--)// {// close(c[num].ctrlfd);// waitpid(c[num].workerid, nullptr, 0);// }// version 1for (const auto &channel : c){close(channel.ctrlfd);waitpid(channel.workerid, nullptr, 0);}// for (const auto &channel : c)// {// pid_t rid = waitpid(channel.workerid, nullptr, 0);// if (rid == channel.workerid)// {// std::cout << "wait child: " << channel.workerid << " success" << std::endl;// }// }
}
int main()
{std::vector<channel> channels;// 1. 创建信道,创建进程CreateChannels(&channels);// 2. 开始发送任务const bool g_always_loop = true;// SendCommand(channels, g_always_loop);SendCommand(channels, !g_always_loop, 10);// 3. 回收资源,想让子进程退出,并且释放管道,只要关闭写端ReleaseChannels(channels);return 0;
}
//Task.hpp
#pragma once#include <iostream>
#include <functional>
#include <vector>
#include <ctime>
#include <unistd.h>// using task_t = std::function<void()>;
typedef std::function<void()> task_t;void Download()
{std::cout << "我是一个下载任务"<< " 处理者: " << getpid() << std::endl;
}void PrintLog()
{std::cout << "我是一个打印日志的任务"<< " 处理者: " << getpid() << std::endl;
}void PushVideoStream()
{std::cout << "这是一个推送视频流的任务"<< " 处理者: " << getpid() << std::endl;
}// void ProcessExit()
// {
// exit(0);
// }class Init
{
public:// 任务码const static int g_download_code = 0;const static int g_printlog_code = 1;const static int g_push_videostream_code = 2;// 任务集合std::vector<task_t> tasks;public:Init(){tasks.push_back(Download);tasks.push_back(PrintLog);tasks.push_back(PushVideoStream);srand(time(nullptr) ^ getpid());}bool CheckSafe(int code){if (code >= 0 && code < tasks.size())return true;elsereturn false;}void RunTask(int code){return tasks[code]();}int SelectTask(){return rand() % tasks.size();}std::string ToDesc(int code){switch (code){case g_download_code:return "Download";case g_printlog_code:return "PrintLog";case g_push_videostream_code:return "PushVideoStream";default:return "Unknow";}}
};Init init; // 定义对象
11.2 system V共享内存
System V 共享内存是一种进程间通信的机制,用于在 Unix 和类 Unix 操作系统中进行共享数据。它允许多个进程访问同一块内存区域,从而实现了高效的数据交换,避免了复制数据的开销。
System V 共享内存通常由以下几个步骤实现:
- 创建共享内存段:
- 首先,一个进程调用
shmget()
系统调用来创建共享内存段。该系统调用需要指定共享内存的大小、权限和一些标志等参数。如果成功创建,shmget()
将返回一个唯一的标识符(通常称为共享内存标识符),用于后续对共享内存的操作。
- 首先,一个进程调用
- 连接到共享内存:
- 进程通过调用
shmat()
系统调用来连接到共享内存段。该系统调用需要传入共享内存标识符以及一些其他参数,用于指定连接的方式。成功连接后,shmat()
返回共享内存段的虚拟内存地址,进程可以通过该地址访问共享内存中的数据。
- 进程通过调用
- 访问共享内存:
- 进程可以像访问普通内存一样,通过指针来读取和写入共享内存中的数据。多个进程可以同时访问同一块共享内存,因此需要使用同步机制(如信号量)来确保数据的一致性和完整性。
- 分离共享内存:
- 当进程不再需要访问共享内存时,可以调用
shmdt()
系统调用将其从当前进程的地址空间中分离。分离后,进程将无法再访问共享内存中的数据,但共享内存本身不会被销毁。
- 当进程不再需要访问共享内存时,可以调用
- 删除共享内存:
- 当所有进程都不再需要共享内存时,可以调用
shmctl()
系统调用来删除共享内存段。这会释放系统资源并销毁共享内存段,以便系统可以重新使用相同的标识符。
- 当所有进程都不再需要共享内存时,可以调用
System V 共享内存提供了一种高效的进程间通信机制,特别适用于需要频繁、大量数据交换的场景。然而,由于共享内存不提供任何同步机制,因此需要结合其他同步机制(如信号量)来确保多个进程对共享内存的安全访问。
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到 内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
11.2.1 system V函数
-
shmget():
int shmget(key_t key, size_t size, int shmflg);
shmget()
函数用于创建一个新的共享内存段或获取一个已存在的共享内存段的标识符。- 参数
key
是用于标识共享内存段的键值,通常可以通过ftok()
函数生成。 - 参数
size
是共享内存段的大小(以字节为单位)。 - 参数
shmflg
是一组标志,用于指定创建共享内存的权限和行为,例如访问权限和创建新段或获取已存在段的方式。 - 如果成功,
shmget()
返回共享内存段的标识符(非负整数),否则返回 -1,并设置errno
以指示错误。
在 System V 共享内存的函数中,
shmflg
参数用于指定创建共享内存段的权限和行为。shmflg
参数通常是一个位掩码,可以通过按位或运算来组合不同的标志。下面介绍一些常用的
shmflg
标志:-
IPC_CREAT:
-
如果指定了该标志,
shmget()
函数将创建一个新的共享内存段,如果共享内存段不存在,则创建之;如果已存在,则返回其标识符。 -
如果未指定该标志,而共享内存段不存在,则
shmget()
返回 -1,并设置errno
为ENOENT
;如果已存在,则返回已存在的共享内存段的标识符。
-
-
IPC_EXCL:
-
与
IPC_CREAT
一起使用时,如果指定了该标志并且共享内存段已经存在,则shmget()
返回 -1,并设置errno
为EEXIST
。 -
该标志通常与
IPC_CREAT
一起使用,用于确保只有一个进程能够创建共享内存段。
-
-
IPC_PRIVATE:
-
指定此标志时,
shmget()
将创建一个新的私有共享内存段,并返回其标识符。该共享内存段对其他进程不可见,只能由当前进程使用。 -
通常不建议直接使用该标志,因为它可能导致内存泄漏或命名冲突等问题。
-
-
权限位:
-
通过指定权限位,可以控制共享内存段的访问权限。通常使用类似于
0666
这样的八进制数字来表示权限,其中6
表示读写权限。 -
例如,
IPC_CREAT | 0666
表示创建共享内存段并设置读写权限。
-
-
shmat():
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmat()
函数用于将当前进程连接到一个共享内存段,并返回共享内存段的地址。- 参数
shmid
是共享内存段的标识符,通常是由shmget()
返回的。 - 参数
shmaddr
是可选的参数,用于指定共享内存段的连接地址,通常设置为 NULL,表示由系统选择一个适当的地址。 - 参数
shmflg
是一组标志,用于指定连接共享内存的行为,例如读写权限和连接方式。 - 如果成功,
shmat()
返回共享内存段的地址,否则返回 -1,并设置errno
以指示错误。
-
shmdt():
int shmdt(const void *shmaddr);
shmdt()
函数用于将当前进程与共享内存段分离,停止访问共享内存段。- 参数
shmaddr
是共享内存段的地址,通常是由shmat()
返回的。 - 如果成功,
shmdt()
返回 0,否则返回 -1,并设置errno
以指示错误。
-
shmctl():
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl()
函数用于控制共享内存段的行为,例如删除共享内存段或获取其状态信息。- 参数
shmid
是共享内存段的标识符。 - 参数
cmd
是一个命令,用于指定要执行的操作,如删除共享内存段、获取状态信息等。 - 参数
buf
是一个指向struct shmid_ds
结构的指针,用于存储共享内存段的状态信息。 - 如果成功,
shmctl()
返回操作的结果,通常为 0 或某个值(取决于操作类型),否则返回 -1,并设置errno
以指示错误。
实例代码:
# makefile
.PHONY:all
all:server clientserver:server.ccg++ -o $@ $^ -std=c++11
client:client.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f server client fifo
// client.cc
#include <iostream>
#include <cstring>
#include <sys/ipc.h> //Inter-Process Communication
#include <sys/shm.h>
#include "comm.hpp"int main()
{key_t key = GetKey();int shmid = GetShm(key);return 0;
}
// comm.hpp
#pragma once#include <iostream>
#include <cstdlib>
#include <string>const std::string pathname = "/home/whb/109/109/lesson30";
const int proj_id = 0x11223344;// 共享内存的大小,强烈建议设置成为n*4096
const int size = 4096; // 4096*2key_t GetKey()
{key_t key = ftok(pathname.c_str(), proj_id);if(key < 0){std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;exit(1);}return key;
}std::string ToHex(int id)
{char buffer[1024];snprintf(buffer, sizeof(buffer), "0x%x", id);return buffer;
}int CreateShmHelper(key_t key, int flag)
{int shmid = shmget(key, size, flag);if(shmid < 0){std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;exit(2);}return shmid;
}int CreateShm(key_t key)
{return CreateShmHelper(key, IPC_CREAT|IPC_EXCL|0644);
}int GetShm(key_t key)
{return CreateShmHelper(key, IPC_CREAT/*0也可以*/);
}
// server.cc
#include <iostream>
#include <cstring>
#include <sys/ipc.h> //Inter-Process Communication
#include <sys/shm.h>
#include <unistd.h>
#include "comm.hpp"int main()
{key_t key = GetKey();std::cout << "key : " << ToHex(key) << std::endl;sleep(3);// key vs shmid// shmid: 应用这个共享内存的时候,我们使用shmid来进行操作共享内存, FILE*// key: 不要在应用层使用,只用来在内核中标识shm的唯一性!, fdint shmid = CreateShm(key);sleep(5);std::cout << "shmid: " << shmid << std::endl;std::cout << "开始将shm映射到进程的地址空间中" << std::endl;char *s = (char*)shmat(shmid, nullptr, 0);sleep(5);shmdt(s);std::cout << "开始将shm从进程的地址空间中移除" << std::endl;sleep(5);shmctl(shmid, IPC_RMID, nullptr);std::cout << "开始将shm从OS中删除" << std::endl;sleep(10);return 0;
}
11.2.2 system 命令
ipcs -m
是一个 Unix/Linux 命令,用于显示共享内存段的信息。它允许用户查看系统中当前存在的共享内存段的相关信息,如共享内存段的标识符、权限、大小、连接的进程等。
以下是 ipcs -m
命令的一般输出格式及其含义:
- 键值(key):用于唯一标识共享内存段的键值。多个进程可以通过相同的键值来访问同一个共享内存段。
- 共享内存标识符(shmid):系统为共享内存段分配的唯一标识符。
- 访问权限(perms):指定了共享内存段的权限,包括读、写和执行权限。
- 连接的进程(cuid/cgid):共享内存段当前连接的进程的用户 ID 和组 ID。
- 连接计数(nattach):表示当前连接到共享内存段的进程数量。
- 大小(size):共享内存段的大小,以字节为单位。
- 最后连接时间(lpid):表示最后一次连接到共享内存段的进程的进程 ID。
- 最后操作时间(cpid):表示最后一次操作(创建、删除等)共享内存段的进程的进程 ID。
通过执行 ipcs -m
命令,用户可以快速了解系统中当前存在的共享内存段的情况,以及哪些进程正在使用这些共享内存段。这对于调试进程间通信问题、优化资源使用以及监控系统状态都非常有用。
ipcrm -m <shmid>
删除共享内存
11.3 system V消息队列
System V 消息队列是一种进程间通信的机制,用于在 Unix 和类 Unix 操作系统中进行数据传递。它允许多个进程之间通过发送和接收消息来进行通信,实现了异步、有序的数据交换。
System V 消息队列通常由以下几个步骤实现:
- 创建消息队列:
- 首先,一个进程调用
msgget()
系统调用来创建一个新的消息队列或获取一个已存在的消息队列的标识符。该系统调用需要指定一个键值来标识消息队列,通常可以通过ftok()
函数生成。
- 首先,一个进程调用
- 发送消息:
- 通过调用
msgsnd()
系统调用来向消息队列发送消息。发送消息时,需要指定消息队列的标识符,以及要发送的消息类型和内容。消息类型通常是一个整数值,用于区分不同类型的消息。
- 通过调用
- 接收消息:
- 通过调用
msgrcv()
系统调用来从消息队列接收消息。接收消息时,需要指定消息队列的标识符、要接收的消息类型、接收消息的缓冲区以及一些其他参数。
- 通过调用
- 删除消息队列:
- 当不再需要消息队列时,可以调用
msgctl()
系统调用来删除消息队列。删除消息队列会释放系统资源,并且所有连接到该消息队列的进程都会收到一个消息队列被删除的通知。
- 当不再需要消息队列时,可以调用
System V 消息队列提供了一种可靠的进程间通信机制,特别适用于需要异步、有序数据交换的场景。它可以用于在不同进程之间传递各种类型的数据,如结构体、字符串等。与共享内存相比,消息队列更适合于一对多或多对多的通信模型,且不需要像共享内存那样考虑数据的一致性和同步问题。
11.4 system V 信号量
System V 信号量是一种进程间同步和互斥的机制,用于在 Unix 和类 Unix 操作系统中进行进程间通信和控制共享资源的访问。它允许多个进程之间通过原子操作来对一个计数器进行操作,从而实现进程之间的同步和互斥。
System V 信号量通常由以下几个步骤实现:
- 创建信号量集:
- 首先,一个进程调用
semget()
系统调用来创建一个新的信号量集或获取一个已存在的信号量集的标识符。该系统调用需要指定一个键值来标识信号量集,通常可以通过ftok()
函数生成。
- 首先,一个进程调用
- 初始化信号量集:
- 通过调用
semctl()
系统调用来初始化信号量集中的各个信号量。初始化信号量时,需要指定信号量的初始值。
- 通过调用
- 操作信号量:
- 通过调用
semop()
系统调用来执行一系列的原子信号量操作。这些操作包括对信号量的增加、减少以及等待等操作。
- 通过调用
- 删除信号量集:
- 当不再需要信号量集时,可以调用
semctl()
系统调用来删除信号量集。删除信号量集会释放系统资源,并且所有连接到该信号量集的进程都会收到一个信号量集被删除的通知。
- 当不再需要信号量集时,可以调用
System V 信号量提供了一种有效的进程间同步和互斥的机制,可用于解决共享资源的并发访问问题。它允许多个进程通过原子操作对一个计数器进行操作,从而实现对共享资源的控制和协调。与锁机制相比,信号量更加灵活,并且可以支持多个进程对同一资源的访问控制。
文章目录
- 11. 进程间通信
- 11.1 管道
- 11.1.0 |
- 11.1.1 匿名管道
- 11.1.2 命名管道
- 11.1.3 用匿名管道形成进程池
- 11.2 system V共享内存
- 11.2.1 system V函数
- 11.2.2 system 命令
- 11.3 system V消息队列
- 11.4 system V 信号量
- 12. 进程信号
- 12.1 前台进程和后台进程
- 12.1.1 jobs
- 12.1.2 fg & bg
- 12.2 硬件中断和软件中断
- 12.3 信号
- 12.3.1 kill -L
- 12.3.2 信号的常见处理方式
- 12.3.3 信号的发送
- 12.3.4 signal
- 12.4 信号的产生
- 12.4.1 通过键盘进行信号产生
- 12.4.1.1 core dump
- 12.4.1.2 SIGINT 和 SIGQUIT
- 12.4.2 通过系统调用指令
- 12.4.3 异常
- 12.4.4 软件条件
- 12.5信号的保存
- 12.5.1 信号的相关概念
- 12.5.2 信号在内核中的表示
- 12.5.3 sigset_t
- 12.5.4 信号集操作函数
- 12.5.5 内核如何实现信号捕捉
- 12.5.6 用户态和内核态
- 12.5.7 sigaction
- 12.5.8 SIGCHILD
- 12.6 可重入函数
- 12.7 volatile
12. 进程信号
进程信号是操作系统中用于通知进程发生了某种事件或异常情况的一种机制。它可以被用来中断进程、传递信息、指示错误等。
比如:SIGINT (2):这是由终端(通常是键盘)发送的中断信号,通常由用户按下Ctrl+C触发。它用于中断当前进程的执行,常用于终止正在运行的程序。
12.1 前台进程和后台进程
在操作系统中,前台进程和后台进程是指进程在与终端交互时的不同状态。
- 前台进程:
- 前台进程是指当前正在与用户交互的进程,它会占据终端的输入和输出。
- 用户可以直接与前台进程交互,输入命令并查看其输出。
- 当用户在终端中启动一个程序时,默认情况下,该程序成为前台进程。
- 后台进程:
- 后台进程是指在终端后台运行的进程,它不会占据终端的输入和输出。
- 用户可以在启动程序时通过特定的命令或操作将其置于后台运行,或者将一个正在前台运行的进程移至后台。
- 后台进程通常在不需要用户交互的情况下运行,允许用户继续在终端执行其他任务。
在Unix/Linux系统中,可以使用以下方式将进程置于后台运行:
- 在命令行末尾添加
&
符号,例如./myprogram &
。 - 使用Ctrl+Z暂停当前前台进程,然后使用
bg
命令将其转为后台运行。 - 使用
nohup
命令启动一个进程,使其在终端关闭后继续在后台运行。
总之,前台进程和后台进程的主要区别在于它们与终端的交互方式,以及用户是否需要等待它们执行完成。
12.1.1 jobs
在Unix和类Unix操作系统(如Linux)中,jobs
命令用于显示当前shell中正在运行或已停止的作业(jobs)。作业可以是前台作业或后台作业。
以下是jobs
命令的一些常见用法和相关信息:
- 显示作业列表:
- 使用
jobs
命令,可以列出当前shell中正在运行或已停止的作业。 - 作业编号会被分配给每个作业,用方括号括起来,例如
[1]
、[2]
等。
- 使用
- 作业状态:
- 作业状态包括正在运行、已停止(挂起)或已完成。
- 每个作业行的开头会显示作业编号、状态(例如Running、Stopped)、PID(进程ID)和作业描述。
- 操作作业:
fg
命令:将一个停止的作业放到前台运行,可以通过指定作业编号或作业描述来选择作业。bg
命令:将一个停止的作业放到后台继续运行,同样可以指定作业编号或作业描述。kill
命令:可以使用kill %作业编号
或kill PID
来结束指定作业或进程。
- 作业控制:
Ctrl+Z
:在前台运行的进程中按下Ctrl+Z
组合键可以将该进程暂停,并将其放入后台作业列表中。Ctrl+C
:在前台运行的进程中按下Ctrl+C
组合键可以中断(终止)该进程。Ctrl+D
:在终端输入行按下Ctrl+D
组合键会发送一个EOF字符,通常用于退出shell或结束输入。
12.1.2 fg & bg
fg
和bg
是Unix和类Unix操作系统(如Linux)中用于作业控制的命令,用于将作业从后台移到前台或者从后台继续在后台运行。
- fg(foreground):
fg
命令用于将一个作业从后台移动到前台运行。- 如果没有指定作业号或作业描述,
fg
命令会将最近的停止的作业(挂起的作业)移到前台继续执行。 - 语法:
fg [作业号或作业描述]
- bg(background):
bg
命令用于将一个停止的作业放到后台继续执行。- 如果没有指定作业号或作业描述,
bg
命令会将最近的停止的作业(挂起的作业)移到后台继续执行。 - 语法:
bg [作业号或作业描述]
使用示例:
-
将最近的停止的作业移到前台执行:
$ fg
-
将编号为1的停止的作业移到前台执行:
$ fg %1
-
将最近的停止的作业移到后台继续执行:
$ bg
-
将编号为2的停止的作业移到后台继续执行:
$ bg %2
这两个命令对于管理终端中运行的作业非常有用,可以方便地切换作业的运行状态。
12.2 硬件中断和软件中断
硬件中断和软件中断都是计算机系统中用于处理异步事件的机制,但它们的触发和处理方式不同。
-
硬件中断:
当我们在程序中发生了除0、访问空指针等非法的操作时,就会引起异常,出发硬件中断被内核捕获,内核会向该进程发送信号终止该进程。
- 硬件中断是由计算机硬件发起的信号,通常用于向处理器(CPU)提出请求或报告事件。
- 例如,外部设备(如键盘、鼠标、硬盘、网络接口卡等)可以向处理器发出中断请求,表示数据已经准备好了,需要被处理。
- 当发生硬件中断时,处理器会停止当前正在执行的任务,并转而执行与中断相关的中断服务程序(ISR,Interrupt Service Routine),来处理中断事件。
- 处理完中断服务程序后,处理器会返回到之前被中断的任务继续执行。
-
软件中断:
- 软件中断是由计算机软件(通常是操作系统或应用程序)通过特殊的指令来触发的中断。
- 软件中断通常用于在程序执行期间主动请求操作系统提供的服务或功能,或者在特定条件下触发一些处理逻辑。
- 软件中断也被称为系统调用(syscall),它们允许用户程序访问操作系统内核提供的服务,如文件操作、网络通信、内存管理等。
- 当发生软件中断时,处理器会类似硬件中断一样,停止当前任务并执行与软件中断相关的中断服务程序,然后再返回到原来的任务继续执行。
总之,硬件中断是由硬件设备发起的异步事件,而软件中断是由软件程序(通常是操作系统或应用程序)发起的异步事件。它们都允许处理器在需要时暂停当前任务并执行特定的处理程序,从而及时响应外部或内部事件。
12.3 信号
12.3.1 kill -L
kill -l
命令用于列出系统支持的所有信号。在Unix和类Unix系统上,通常用于查看可用的信号列表及其对应的编号。
使用kill -l
命令可以获得类似以下的输出:
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE9) 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
35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4
39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12
47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14
51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10
55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6
59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
每个信号都有一个唯一的编号和一个对应的名称。例如,SIGINT
表示中断信号,通常由用户按下Ctrl+C键触发。SIGKILL
表示强制终止信号,用于立即结束一个进程等等。
在Linux系统中,信号的编号范围通常是1到31。这些信号的含义是由POSIX标准定义的,是标准的UNIX信号。然而,32到63之间的信号编号通常被用于扩展,称为实时信号
(Real-time signals)。
12.3.2 信号的常见处理方式
- 忽略此信号。
- 执行该信号的默认处理动作。
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。
12.3.3 信号的发送
- 信号的产生、保存和处理:
- 信号的产生是指由硬件、软件或操作系统等外部或内部事件触发,向进程发送信号的过程。
- 进程接收到信号后,可能由于当前任务优先级较高等原因,无法立即处理该信号,此时进程会将信号暂时保存起来。
- 进程的信号处理过程包括将信号保存、等待适当的时机处理信号等步骤。
- 信号的记录与管理:
- 进程通过维护一个信号位图(signal bitmap)来记录接收到的信号。
- 信号位图中的比特位位置对应信号的编号,比特位内容(0或1)表示是否收到该信号。
- 当进程接收到信号时,相应信号位图中的比特位会被置位,表示该信号已经被接收到。
- 信号的发送过程:
- 信号的发送是由操作系统完成的,进程无法直接操作信号位图。
- 进程通过系统调用或其他方式向操作系统发出信号请求,由操作系统来处理信号的发送和处理过程。
- 信号处理是进程与操作系统之间的重要交互方式,允许进程在异步事件发生时进行及时响应和处理。
- 通过信号,进程可以接收来自硬件、其他进程或操作系统的通知,从而实现进程间通信和协作。
12.3.4 signal
signal
是一个用于处理信号的函数,在C语言中定义在 <signal.h>
头文件中。它用于设置信号处理器(signal handler),即当接收到特定信号时要执行的函数。
下面是signal
函数的原型:
void (*signal(int signum, void (*handler)(int)))(int);
其中,signum
参数表示要设置处理器的信号编号,handler
参数表示要执行的信号处理函数。
signal
函数有以下几种用法:
- 设置信号处理器:
- 可以通过调用
signal(signum, handler)
函数来设置特定信号的处理器。 signum
表示要设置处理器的信号编号,handler
表示要执行的信号处理函数。
- 可以通过调用
- 获取当前信号处理器:
- 如果
handler
参数为SIG_DFL
,则表示恢复默认的信号处理方式。 - 如果
handler
参数为SIG_IGN
,则表示忽略该信号。 - 如果
handler
参数为其他函数指针,则表示设置自定义的信号处理函数。
- 如果
- 返回值:
signal
函数返回值为之前与该信号相关联的处理器的函数指针。如果之前没有设置处理器,则返回值为SIG_DFL
。
在使用signal
函数设置信号处理器时,需要注意以下几点:
- 信号处理函数的参数类型为
int
,表示接收到的信号编号。 - 信号处理函数的返回类型为
void
。 - 在信号处理函数中,通常采取简单的处理方式,如设置标志位,在主程序中定期检查标志位并进行相应的处理。
总的来说,signal
函数是C语言中用于设置信号处理器的主要函数,通过它可以为特定信号设置自定义的处理函数,实现对信号的响应和处理。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>// 信号处理函数
void sigint_handler(int signum) {printf("Received SIGINT signal.\n");// 这里可以添加信号处理逻辑,比如设置标志位或执行清理操作exit(signum); // 退出程序
}int main() {// 设置信号处理器if (signal(SIGINT, sigint_handler) == SIG_ERR) {perror("Error setting signal handler");return 1;}// 主程序printf("Press Ctrl+C to send SIGINT signal.\n");while (1) {// 主程序持续执行}return 0;
}
在这个示例中,我们首先定义了一个名为 sigint_handler
的信号处理函数,当接收到 SIGINT
信号时将会调用该函数。然后,在 main
函数中,我们使用 signal(SIGINT, sigint_handler)
设置了 SIGINT
信号的处理器为我们定义的 sigint_handler
函数。
在 main
函数的主循环中,程序持续执行某些操作。当用户按下 Ctrl+C 时,会触发 SIGINT
信号,从而调用 sigint_handler
函数来处理该信号。在这个示例中,我们在 sigint_handler
函数中打印一条消息并退出程序,你也可以在这里执行其他的信号处理逻辑。
注意:9号信号是不能被捕捉、阻塞和忽略的。
12.4 信号的产生
12.4.1 通过键盘进行信号产生
- 当用户按下键盘上的组合键(比如Ctrl+C)时,操作系统会发送一个中断信号(通常是SIGINT)给与当前终端关联的前台进程。
- 这是一个用户主动触发的信号产生过程,通常用于终止当前正在运行的程序。
12.4.1.1 core dump
Core dump 是指在程序异常终止时,操作系统自动生成的一个包含程序当前内存状态的文件。这个文件通常被称为核心转储文件(core dump file),用于记录程序异常终止时的内存信息,以便后续的调试分析。
以下是关于 core dump 的一些重要信息:
- 生成原因:
- 当一个程序发生严重错误或遇到无法处理的异常情况时,操作系统会将程序当前的内存状态保存到核心转储文件中。
- 典型的生成原因包括访问非法内存、除以零、段错误、内存溢出等。
- 包含内容:
- 核心转储文件记录了程序在异常终止时的内存状态,包括进程的内存映像、寄存器状态、堆栈信息等。
- 这些信息对于分析程序崩溃的原因、查找 bug 和进行调试非常有用。
- 调试分析:
- 开发人员可以使用调试工具(如 GDB)加载核心转储文件,从而在程序崩溃的状态下进行调试。
- 通过分析核心转储文件,开发人员可以确定程序崩溃的原因、定位问题代码,并进行修复。
- 保护机制:
- 在生产环境中,通常会关闭核心转储功能,以防止敏感信息泄露。
- 可以通过设置操作系统或应用程序的配置来控制是否生成核心转储文件。
操作系统可能会设置 ulimit
(用户资源限制)来限制核心转储文件的大小,以避免占用过多磁盘空间。
在Unix-like系统中,可以通过在程序中调用 ulimit
设置允许生成核心转储文件,或者在终端运行程序时使用 ulimit -c unlimited
临时修改。
12.4.1.2 SIGINT 和 SIGQUIT
SIGINT 和 SIGQUIT 是 Unix 系统中的两个常见信号,它们的默认处理动作确实是不同的,下面逐个解释:
- SIGINT(中断信号):
- 默认处理动作是终止进程。
- SIGINT 通常是由用户在终端上按下 Ctrl+C 键产生的,用于终止正在运行的程序或进程。
- 当进程收到 SIGINT 信号时,默认行为是立即终止进程的执行,并回到控制台命令行提示符下。
- SIGQUIT(退出信号):
- 默认处理动作是终止进程并生成核心转储文件(Core Dump)。
- SIGQUIT 通常是由用户在终端上按下 Ctrl+\ 键产生的,用于在终止进程的同时生成核心转储文件以便进行调试分析。
- 当进程收到 SIGQUIT 信号时,默认行为是终止进程的执行,并生成一个核心转储文件,以记录进程当前的内存状态。
12.4.2 通过系统调用指令
- 进程可以通过系统调用(如
kill()
函数)向其他进程发送信号。 - 通过
kill()
函数,进程可以向目标进程发送各种不同的信号,比如终止信号(SIGTERM)、强制终止信号(SIGKILL)等。 - 这种方式是一种进程间通信的方式,允许一个进程向另一个进程发送信号,从而影响其行为。
12.4.3 异常
- 异常是由于程序错误、非法操作或硬件故障等导致的意外情况。
- 当进程执行过程中发生异常时,操作系统会向进程发送相应的信号,以通知进程发生了异常情况。
- 例如,当进程尝试访问未分配内存或发生除以零的操作时,操作系统会向进程发送段错误信号(SIGSEGV)或浮点异常信号(SIGFPE)等。
12.4.4 软件条件
- 进程可以通过设置定时器来在未来的某个时间点触发信号。
- 使用
alarm()
函数可以在一定时间后向进程发送SIGALRM
信号。 - 这种方式常用于实现超时机制或定时任务,在指定时间后向进程发送信号,以触发相应的处理逻辑。
aise() 函数:
-
raise()
函数用于向当前进程发送指定的信号。 -
函数原型为
int raise(int sig)
,其中sig
参数表示要发送的信号。 -
当成功发送信号时,
raise()
函数返回 0;如果失败,则返回非零值。 -
raise()
函数通常用于在程序中模拟信号的产生,或者手动触发某个信号来测试信号处理函数。
abort() 函数:
-
abort()
函数用于使当前进程异常终止,并向操作系统发送SIGABRT
信号。 -
调用
abort()
函数会导致程序立即退出,并生成一个核心转储文件(core dump),用于调试分析程序崩溃的原因。 -
SIGABRT
信号通常用于表示程序遇到严重错误或不可恢复的情况,需要立即终止执行。
12.5信号的保存
12.5.1 信号的相关概念
在信号处理过程中,信号的状态可以分为三种:未决(pending)、递达(delivered)和处理(handled)。当信号产生后,首先处于未决状态,然后在处理之前递达给进程,最后进入处理过程。
- 信号的产生:
- 信号的产生是指由于某个事件或条件发生而导致向进程发送信号的过程。例如,按下Ctrl+C键产生中断信号SIGINT。
- 信号的未决状态(pending):
- 信号的未决状态是指在信号产生后,但尚未递达给进程的状态。这时,操作系统知道信号已经产生,但还未通知进程。
- 未决状态的信号可以被进程阻塞,也可以不被阻塞,取决于进程对该信号的阻塞设置。
- 信号的递达(delivered):
- 信号的递达是指信号已经被操作系统发送给了进程,进程收到了信号。此时,信号从未决状态转变为递达状态。
- 递达的信号会在进程的信号位图中被设置为“递达”状态,等待进程处理。
- 信号的处理(handled):
- 信号的处理是指进程执行与该信号相关联的信号处理函数(也称为信号处理器)的过程。
- 一旦进程收到信号并处理完成,该信号就被认为已经被处理。
- 阻塞某个信号:
- 进程可以选择阻塞某个信号,即暂时屏蔽对该信号的递达。
- 当某个信号被阻塞时,即使它已经递达给进程,进程也不会立即处理该信号,而是将其保留在未决状态,直到解除阻塞。
- 通过阻塞某个信号,进程可以控制哪些信号需要立即处理,哪些信号可以延迟处理,从而更好地控制进程的行为和响应。
- 信号的忽略:
- 信号的忽略是指进程选择忽略特定信号的处理。当进程收到一个被设置为忽略的信号时,该信号不会触发任何默认行为或用户定义的信号处理函数,而是被完全忽略。这意味着进程不会对该信号做出任何响应,信号被丢弃,进程继续执行当前的任务。
- 进程可以通过调用
signal()
函数将信号的处理方式设置为SIG_IGN
,表示忽略该信号。 - 也可以通过调用
sigaction()
函数设置sa_handler
字段为SIG_IGN
来实现信号的忽略。
12.5.2 信号在内核中的表示
- 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
- SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
12.5.3 sigset_t
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
12.5.4 信号集操作函数
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
- 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
- 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
sigprocmask
和 sigpending
是 Unix 系统中用于信号管理的两个函数,它们用于分别设置进程的信号屏蔽集和查询当前被阻塞的待处理信号集。下面逐个介绍:
-
sigprocmask:
-
sigprocmask
函数用于设置进程的信号屏蔽集,即阻塞或解除阻塞特定的信号。 -
函数原型为
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
。 -
how
参数指定了设置信号屏蔽集的方式,可以取三个值之一:
SIG_BLOCK
:将set
中的信号添加到当前的信号屏蔽集中。SIG_UNBLOCK
:从当前的信号屏蔽集中移除set
中的信号。SIG_SETMASK
:将当前的信号屏蔽集替换为set
。
-
set
参数指定了要设置的信号集。 -
oldset
参数用于存储调用该函数之前的信号屏蔽集。
-
-
sigpending:
sigpending
函数用于查询当前被阻塞的待处理信号集。- 函数原型为
int sigpending(sigset_t *set)
。 set
参数用于存储当前被阻塞的待处理信号集。- 调用
sigpending
函数后,set
中会包含当前被阻塞的待处理信号集的信息,可以进一步分析处理这些信号。
12.5.5 内核如何实现信号捕捉
- 注册信号处理函数:
- 当进程调用
signal()
函数或者sigaction()
函数注册信号处理函数时,内核将保存该信号的处理函数信息,并在发生该信号时调用相应的处理函数。 - 这些信号处理函数可以是用户自定义的函数,也可以是特定的预定义处理方式,如忽略信号、终止进程等。
- 当进程调用
- 设置进程的信号掩码:
- 每个进程都有一个信号掩码(signal mask),用于控制哪些信号被阻塞,哪些信号可以被接收。
- 当进程调用
sigprocmask()
函数设置信号掩码时,内核将根据参数指定的方式来修改进程的信号掩码。
- 触发信号:
- 当系统中发生某些特定的事件或条件时,如按下 Ctrl+C 键、子进程状态改变等,内核会向目标进程发送相应的信号。
- 如果目标进程已经注册了对应信号的处理函数,并且该信号未被信号掩码阻塞,内核将调用该处理函数来处理信号。
- 调用信号处理函数:
- 当内核接收到信号后,会根据进程的注册信息,调用对应的信号处理函数。
- 内核会将当前进程的上下文保存起来,然后转而执行信号处理函数。
- 在信号处理函数执行完成后,内核会恢复进程的上下文,并让进程继续执行。
12.5.6 用户态和内核态
信号的处理涉及到用户态和内核态两个不同的执行环境,这取决于信号的产生和处理的具体过程。
- 用户态:
- 用户态是指进程在执行用户程序时所处的状态。在用户态,进程可以访问受限资源,如进程的用户空间内存。
- 当进程处于用户态时,如果产生了信号并且信号没有被阻塞,操作系统会通知进程收到了信号,但进程的信号处理函数不会立即执行。
- 进程在用户态接收到信号后,只有在发生系统调用、异常或中断时,才会进入内核态执行信号处理函数。
- 内核态:
- 内核态是指操作系统内核执行时所处的状态。在内核态,操作系统可以访问系统的全部资源,如CPU、内存、设备等。
- 当进程在用户态接收到信号后,如果发生了系统调用、异常或中断,操作系统会将进程切换到内核态,并在内核态执行与该信号相关联的信号处理函数。
- 在内核态执行信号处理函数时,操作系统可以直接访问进程的内存空间和其他资源,执行与信号相关的操作,如关闭文件、终止进程等。
12.5.7 sigaction
sigaction()
是 Unix 系统中用于设置和检索信号处理器(signal handler)的系统调用函数。它提供了更灵活和可靠的信号处理方式,相比较于旧的 signal()
函数,sigaction()
函数具有更多的参数和选项,可以更精确地控制信号处理的行为。
下面是 sigaction()
函数的主要特点和使用方法:
-
函数原型:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
-
参数说明:
signum
:要设置或检索处理器的信号编号。act
:指向struct sigaction
结构的指针,包含了要设置的信号处理器的信息。oldact
:指向struct sigaction
结构的指针,用于存储之前设置的信号处理器的信息(可选)。
-
struct sigaction 结构:
struct sigaction
结构用于指定信号的处理器及相关选项,包括以下字段:sa_handler
:指定信号的处理函数,可以是函数指针或者SIG_IGN
、SIG_DFL
。sa_sigaction
:指定信号的扩展处理函数,与sa_handler
互斥。sa_mask
:指定在处理信号期间要阻塞的附加信号集。sa_flags
:指定额外的处理标志,如SA_RESTART
。
-
返回值:
- 如果函数调用成功,返回 0;如果出现错误,返回 -1,并设置
errno
变量指示错误类型。
- 如果函数调用成功,返回 0;如果出现错误,返回 -1,并设置
通过 sigaction()
函数,可以实现对特定信号的更加精细和可靠的处理方式。它可以用于注册信号处理函数、指定处理器执行期间要阻塞的其他信号、设置信号处理器的一些额外标志等。因此,sigaction()
函数比 signal()
函数更适合于在 Unix 程序中进行信号处理。
12.5.8 SIGCHILD
SIGCHLD
是一个在 Unix 系统中的信号,表示子进程状态发生改变,通常是子进程退出或停止。以下是关于 SIGCHLD
信号的一些重要信息:
- 信号编号:
SIGCHLD
的编号通常为 17 或 18。
- 产生原因:
SIGCHLD
信号通常是由子进程的状态改变而产生的。这种状态改变可能是子进程正常退出、异常退出、收到停止信号或继续信号等。- 当子进程状态发生改变时,操作系统会向父进程发送
SIGCHLD
信号,以通知父进程子进程的状态变化。
- 处理方式:
- 大多数情况下,父进程需要处理
SIGCHLD
信号以获取子进程的退出状态信息,并执行必要的清理工作。 - 父进程可以通过设置信号处理函数来处理
SIGCHLD
信号。在信号处理函数中,父进程通常会调用wait()
或waitpid()
等系统调用来等待子进程退出,并获取子进程的退出状态。 - 如果父进程没有显式处理
SIGCHLD
信号,那么操作系统会将子进程设置为僵尸进程(zombie process),直到父进程主动处理该信号或退出。
- 大多数情况下,父进程需要处理
- 用途:
SIGCHLD
信号的主要作用是通知父进程子进程的状态变化,使父进程能够及时处理子进程的退出状态,防止子进程成为僵尸进程。- 父进程可以通过
SIGCHLD
信号来监控和管理多个子进程的生命周期,以实现并发执行和资源管理。
总的来说,SIGCHLD
信号在 Unix 系统中是用来通知父进程子进程状态改变的重要信号,父进程通常需要显式处理该信号以获取子进程的退出状态,并执行必要的清理工作,以确保系统的稳定性和安全性。
12.6 可重入函数
可重入函数(reentrant function)是指在多线程或信号处理程序中能够安全地被并发调用的函数。
和信号的关系:
- 信号处理函数应当是可重入的:
- 在信号处理函数中,通常需要谨慎地编写代码,以确保函数是可重入的。
- 当进程正在执行一个信号处理函数时,如果同时接收到另一个相同的信号,那么新的信号处理函数可能会在原来的信号处理函数执行的过程中被调用。
- 如果信号处理函数不是可重入的,可能会导致竞态条件、数据损坏或未定义的行为。
- 可重入函数可以安全地在信号处理函数中调用:
- 可重入函数不依赖于全局状态或静态变量,而是依赖于函数参数和本地变量。
- 这意味着可重入函数可以安全地在信号处理函数中调用,而不会引发竞态条件或不确定的行为。
- 在信号处理函数中使用可重入函数是一种良好的做法,因为它能够提高信号处理函数的安全性和可靠性。
12.7 volatile
volatile
是 C 和 C++ 中的一个关键字,用于告诉编译器不要优化某个变量的读取或写入操作,以确保每次对该变量的访问都是真实的、未知的、可能会改变的。volatile
主要用于以下两种情况:
- 访问硬件或外部设备的状态:
- 当一个变量代表硬件寄存器或外部设备的状态时,该变量可能在任何时刻被修改。
- 在这种情况下,使用
volatile
告诉编译器不要对该变量的读取或写入进行优化,以确保读取或写入操作都是真实的。
- 访问多线程共享的变量:
- 当一个变量在多个线程之间共享,并且可能被其他线程异步地修改时,需要使用
volatile
来确保对该变量的读取和写入操作都是可见的。 - 在这种情况下,
volatile
可以帮助防止编译器进行过多的优化,以保证线程之间的同步和可见性。
- 当一个变量在多个线程之间共享,并且可能被其他线程异步地修改时,需要使用
需要注意的是,虽然 volatile
可以确保变量的读取和写入是真实的,但它并不能保证变量的操作是原子的。在多线程环境下,如果需要保证原子性操作,应该使用专门的原子操作函数或者同步机制(如互斥锁、信号量等)来确保线程安全。
总的来说,volatile
关键字告诉编译器不要对变量的读取或写入进行优化,适用于访问硬件状态或外部设备的变量,以及多线程共享的变量。