当前位置: 首页 > news >正文

网站内容不收录贵阳网站建设gzzctyi

网站内容不收录,贵阳网站建设gzzctyi,用wordpress建仿站,ppt做视频 模板下载网站文章目录 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 jobs12.1.2 fg bg 12.2 硬件中断和软件中断12.3 信号12.3.1 kill -L12.3.2 信号的常见处理方式12.3.3 信号的发送12.3.4 signal 12.4 信号的产生12.4.1 通过键盘进行信号产生12.4.1.1 core dump12.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_t12.5.4 信号集操作函数12.5.5 内核如何实现信号捕捉12.5.6 用户态和内核态12.5.7 sigaction12.5.8 SIGCHILD12.6 可重入函数12.7 volatile 11. 进程间通信 进程间通信Inter-Process CommunicationIPC是指不同进程之间进行数据交换和同步的机制。在多任务操作系统中多个进程可能同时存在并运行它们之间可能需要相互通信以完成某些任务。IPC 提供了一种机制允许进程之间传递信息、共享数据并在必要时进行同步。 为了让两个进程之间进行通信首先需要让它们看到同一份资源。 以下是几种常见的进程间通信方式 管道Pipes 匿名管道用于具有亲缘关系的进程间的通信如父进程与子进程之间。它是单向的数据只能单向流动。Linux/Unix系统中可以通过 pipe() 系统调用创建管道。 命名管道Named Pipes 命名管道允许任何进程之间进行通信即使它们没有亲缘关系。命名管道是一种特殊的文件系统对象。在Linux/Unix中使用 mkfifo 命令可以创建命名管道。 消息队列Message Queues 消息队列允许进程通过向队列发送和接收消息来通信。它们是在内核中维护的消息链表每个消息都有一个类型以及一个用于标识消息的整数标识符。在Linux/Unix系统中可以使用 msgget()、msgsnd() 和 msgrcv() 等函数来操作消息队列。 共享内存Shared Memory 共享内存允许两个或多个进程共享同一块内存区域。这对于需要高性能数据交换的进程非常有用因为它避免了数据的复制。在Linux/Unix中可以使用 shmget()、shmat() 和 shmdt() 等函数来创建和操作共享内存区域。 信号量Semaphores 信号量是一个计数器用于控制多个进程对共享资源的访问。它可以用于解决竞争条件和同步问题。在Linux/Unix中可以使用 semget()、semop() 和 semctl() 等函数来创建和操作信号量。 进程间通信的目的 数据传输一个进程需要将它的数据发送给另一个进程资源共享多个进程之间共享同样的资源。通知事件一个进程需要向另一个或一组进程发送消息通知它它们发生了某种事件如进程终止时要通知父进程。进程控制有些进程希望完全控制另一个进程的执行如Debug进程此时控制进程希望能够拦截另一个进程的所有陷入和异常并能够及时知道它的状态改变。 进程间通信的分类 管道Pipes 匿名管道pipe和命名管道named pipe是两种常见的管道形式用于在具有亲缘关系或非亲缘关系的进程之间进行通信。管道是单向的只能支持单向数据流动。 System V IPC System V IPC 是一套用于进程间通信的标准提供了三种主要的通信机制消息队列、共享内存和信号量。这些机制在早期Unix系统上广泛使用但在现代系统中可能已经被POSIX IPC替代。 System V 消息队列允许进程通过消息队列发送和接收消息消息队列在内核中维护进程通过系统调用进行操作。System V 共享内存允许多个进程访问同一块共享内存区域从而实现快速的数据交换。进程通过获取共享内存的标识符来访问共享内存区域。System V 信号量信号量用于控制多个进程对共享资源的访问。它可以用于实现互斥和同步操作以防止竞争条件的发生。 POSIX IPC POSIX IPC 是一套与POSIX标准兼容的进程间通信机制与System V IPC相比它更加简单、灵活并且更容易使用。它包括消息队列、共享内存、信号量、互斥量、条件变量和读写锁等机制。 消息队列与System V消息队列类似允许进程通过消息队列发送和接收消息。共享内存与System V共享内存类似允许多个进程访问同一块共享内存区域。信号量与System V信号量类似用于控制多个进程对共享资源的访问。互斥量Mutex用于实现互斥访问共享资源只能被一个线程持有。条件变量Condition Variable用于线程间的同步当满足特定条件时允许线程挂起或唤醒。读写锁Read-Write Lock用于控制对共享资源的读写访问允许多个线程同时读取但只允许一个线程进行写入。 进程间通信的本质让不同进程先看到同一份资源 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 -l11.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: (status0x7F) endl;}return 0; }用fork来共享管道原理 站在文件描述符角度-深度理解管道 11.1.2 命名管道 命名管道Named Pipes也称为 FIFOFirst-In-First-Out是一种在Unix/Linux系统中用于进程间通信的机制。与匿名管道不同命名管道允许任何进程之间进行通信即使它们没有亲缘关系。 命名管道是一种特殊的文件系统对象它有一个路径名并且可以像文件一样被打开、读取和写入。命名管道通过文件系统在磁盘上创建因此可以在文件系统中存在可以通过文件系统的路径进行访问。 使用命名管道的一般流程如下 创建命名管道可以使用 mkfifo 命令或者 mkfifo() 系统调用来创建命名管道。例如mkfifo /path/to/fifo。打开命名管道进程可以像打开普通文件一样打开命名管道使用文件路径来打开它。进行读写操作打开命名管道后进程就可以向管道中写入数据或者从管道中读取数据。管道的数据传输遵循先进先出FIFO的原则。关闭命名管道当通信完成后进程应该关闭命名管道释放资源。 命名管道的特点包括 允许任意进程之间进行通信即使它们没有亲缘关系。类似于普通文件可以通过文件系统的路径进行访问。数据传输遵循先进先出FIFO的原则。 命名管道通常用于需要持续进行数据交换的进程之间的通信例如在客户端和服务器之间传输数据或者在不同的进程间进行数据传输和处理。 mkfifo()和mkfifo mkfifo() 是一个Unix/Linux系统调用用于创建命名管道Named Pipe也称为 FIFOFirst-In-First-Out。命名管道是一种进程间通信机制允许任意进程之间进行通信即使它们没有亲缘关系。 mkfifo() 的声明如下 #include sys/types.h #include sys/stat.hint mkfifo(const char *pathname, mode_t mode);其中pathname 是命名管道的路径名mode 是创建命名管道的权限位。函数调用成功时返回 0失败时返回 -1并设置合适的错误码以指示失败原因。 mkfifo 命令是在Unix/Linux系统中用于创建命名管道的命令行工具。它的基本语法如下 mkfifo [options] name...其中name 是要创建的命名管道的名称可以指定多个名称。mkfifo 命令创建的命名管道将会在当前目录下创建。一旦创建成功其他进程就可以通过文件系统路径名来访问这些命名管道。 命令行示例 终端1while :; do echo “hello world”;sleep 1;done fifo 终端2cat fifo 实例代码 # makefile .PHONY:all all:server clientserver:server.ccg -o $ $^ -stdc11 client:client.ccg -o $ $^ -stdc11 .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.hint 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.hbool 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 $ $^ -stdc11 .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.hppconst 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::vectorint fds) {std::cout getpid() close fds: ;for(auto fd : fds){std::cout fd ;}std::cout std::endl; }// 传参形式 // 1. 输入参数const // 2. 输出参数* // 3. 输入输出参数 void CreateChannels(std::vectorchannel *c) {// bugstd::vectorint 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::vectorchannel c) {for (const auto channel : c){std::cout channel.name , channel.ctrlfd , channel.workerid std::endl;} }void SendCommand(const std::vectorchannel 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::vectorchannel 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::vectorchannel 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::functionvoid(); typedef std::functionvoid() 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::vectortask_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 $ $^ -stdc11 client:client.ccg -o $ $^ -stdc11 .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.hppint main() {key_t key GetKey();int shmid GetShm(key);return 0; }// comm.hpp #pragma once#include iostream #include cstdlib #include stringconst 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.hppint 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 jobs12.1.2 fg bg 12.2 硬件中断和软件中断12.3 信号12.3.1 kill -L12.3.2 信号的常见处理方式12.3.3 信号的发送12.3.4 signal 12.4 信号的产生12.4.1 通过键盘进行信号产生12.4.1.1 core dump12.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_t12.5.4 信号集操作函数12.5.5 内核如何实现信号捕捉12.5.6 用户态和内核态12.5.7 sigaction12.5.8 SIGCHILD12.6 可重入函数12.7 volatile 12. 进程信号 进程信号是操作系统中用于通知进程发生了某种事件或异常情况的一种机制。它可以被用来中断进程、传递信息、指示错误等。 比如SIGINT (2)这是由终端通常是键盘发送的中断信号通常由用户按下CtrlC触发。它用于中断当前进程的执行常用于终止正在运行的程序。 12.1 前台进程和后台进程 在操作系统中前台进程和后台进程是指进程在与终端交互时的不同状态。 前台进程 前台进程是指当前正在与用户交互的进程它会占据终端的输入和输出。用户可以直接与前台进程交互输入命令并查看其输出。当用户在终端中启动一个程序时默认情况下该程序成为前台进程。 后台进程 后台进程是指在终端后台运行的进程它不会占据终端的输入和输出。用户可以在启动程序时通过特定的命令或操作将其置于后台运行或者将一个正在前台运行的进程移至后台。后台进程通常在不需要用户交互的情况下运行允许用户继续在终端执行其他任务。 在Unix/Linux系统中可以使用以下方式将进程置于后台运行 在命令行末尾添加符号例如./myprogram 。使用CtrlZ暂停当前前台进程然后使用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来结束指定作业或进程。 作业控制 CtrlZ在前台运行的进程中按下CtrlZ组合键可以将该进程暂停并将其放入后台作业列表中。CtrlC在前台运行的进程中按下CtrlC组合键可以中断终止该进程。CtrlD在终端输入行按下CtrlD组合键会发送一个EOF字符通常用于退出shell或结束输入。 12.1.2 fg bg fg和bg是Unix和类Unix操作系统如Linux中用于作业控制的命令用于将作业从后台移到前台或者从后台继续在后台运行。 fgforeground fg命令用于将一个作业从后台移动到前台运行。如果没有指定作业号或作业描述fg命令会将最近的停止的作业挂起的作业移到前台继续执行。语法fg [作业号或作业描述] bgbackground bg命令用于将一个停止的作业放到后台继续执行。如果没有指定作业号或作业描述bg命令会将最近的停止的作业挂起的作业移到后台继续执行。语法bg [作业号或作业描述] 使用示例 将最近的停止的作业移到前台执行 $ fg将编号为1的停止的作业移到前台执行 $ fg %1将最近的停止的作业移到后台继续执行 $ bg将编号为2的停止的作业移到后台继续执行 $ bg %2这两个命令对于管理终端中运行的作业非常有用可以方便地切换作业的运行状态。 12.2 硬件中断和软件中断 硬件中断和软件中断都是计算机系统中用于处理异步事件的机制但它们的触发和处理方式不同。 硬件中断 当我们在程序中发生了除0、访问空指针等非法的操作时就会引起异常出发硬件中断被内核捕获内核会向该进程发送信号终止该进程。 硬件中断是由计算机硬件发起的信号通常用于向处理器CPU提出请求或报告事件。例如外部设备如键盘、鼠标、硬盘、网络接口卡等可以向处理器发出中断请求表示数据已经准备好了需要被处理。当发生硬件中断时处理器会停止当前正在执行的任务并转而执行与中断相关的中断服务程序ISRInterrupt 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) SIGRTMIN1 36) SIGRTMIN2 37) SIGRTMIN3 38) SIGRTMIN4 39) SIGRTMIN5 40) SIGRTMIN6 41) SIGRTMIN7 42) SIGRTMIN8 43) SIGRTMIN9 44) SIGRTMIN10 45) SIGRTMIN11 46) SIGRTMIN12 47) SIGRTMIN13 48) SIGRTMIN14 49) SIGRTMIN15 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表示中断信号通常由用户按下CtrlC键触发。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 CtrlC to send SIGINT signal.\n);while (1) {// 主程序持续执行}return 0; } 在这个示例中我们首先定义了一个名为 sigint_handler 的信号处理函数当接收到 SIGINT 信号时将会调用该函数。然后在 main 函数中我们使用 signal(SIGINT, sigint_handler) 设置了 SIGINT 信号的处理器为我们定义的 sigint_handler 函数。 在 main 函数的主循环中程序持续执行某些操作。当用户按下 CtrlC 时会触发 SIGINT 信号从而调用 sigint_handler 函数来处理该信号。在这个示例中我们在 sigint_handler 函数中打印一条消息并退出程序你也可以在这里执行其他的信号处理逻辑。 注意9号信号是不能被捕捉、阻塞和忽略的。 12.4 信号的产生 12.4.1 通过键盘进行信号产生 当用户按下键盘上的组合键比如CtrlC时操作系统会发送一个中断信号通常是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 通常是由用户在终端上按下 CtrlC 键产生的用于终止正在运行的程序或进程。当进程收到 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。当信号产生后首先处于未决状态然后在处理之前递达给进程最后进入处理过程。 信号的产生 信号的产生是指由于某个事件或条件发生而导致向进程发送信号的过程。例如按下CtrlC键产生中断信号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 sigismemberconst 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() 函数设置信号掩码时内核将根据参数指定的方式来修改进程的信号掩码。 触发信号 当系统中发生某些特定的事件或条件时如按下 CtrlC 键、子进程状态改变等内核会向目标进程发送相应的信号。如果目标进程已经注册了对应信号的处理函数并且该信号未被信号掩码阻塞内核将调用该处理函数来处理信号。 调用信号处理函数 当内核接收到信号后会根据进程的注册信息调用对应的信号处理函数。内核会将当前进程的上下文保存起来然后转而执行信号处理函数。在信号处理函数执行完成后内核会恢复进程的上下文并让进程继续执行。 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 变量指示错误类型。 通过 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 关键字告诉编译器不要对变量的读取或写入进行优化适用于访问硬件状态或外部设备的变量以及多线程共享的变量。
http://www.zqtcl.cn/news/413647/

相关文章:

  • 上街郑州网站建设网站管理建设的需求分析
  • 厦门网站建设策划网站推广的常用方法有哪些
  • 做电脑图标的网站上海定制网站建设公司哪家好
  • 重庆seo网站推广工具济南网页设计师招聘信息
  • 甘肃永靖建设住建局网站深圳网络广告推广公司
  • 台州企业网站搭建电话厦门学网站建设
  • 做易经网站做网站布为网
  • 高端定制开发网站可以做网站的网络
  • 局政务网站建设管理工作总结wordpress ks主题
  • 网站集约化建设的意义网页制作成app
  • 建设银行大厂支行网站专业的营销型网站建设公司
  • 询盘网站苏州建设银行招聘网站
  • 制作网站图片手机网站跳转
  • 装修公司营销网站模板东莞家居网站建设
  • 网站模板建站教程视频德州极速网站建设百家号
  • 专做蔬菜水果的网站自学it从哪里学起
  • 邵阳红网站搭建平台聚合力
  • 滁州网站建设信息推荐软件开发技术方案模板
  • 商务网站建设有哪几个步骤拼多多网页qq登录
  • 厦门商城网站开发宜昌小程序开发公司
  • 东莞沙田网站建设榆林网站建设价格
  • 无锡网站制作建设wordpress写文章模板
  • 企业网站销售提升学历要多少钱
  • 打开建设银行官方网站首页wordpress 站库分离
  • 电子商务网站建设的试卷设计之家app
  • 抚养网站建设黔东南小程序开发公司
  • 网站建设相关行业有哪些wordpress 内容管理系统
  • 网站 备案地温州网站优化排名推广
  • 做网站的工作量国内 wordpress
  • 定制网站开发是什么大业推广网站