Linux服务器项目-5
2.7 信号
信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
- 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入 Ctrl+C通常会给进程发送一个中断信号。
- 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域。
- 系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU时间超限,或者该进程的某个子进程退出。
- 运行 kill 命令或调用 kill 函数。
使用信号的两个主要目的是:
- 让进程知道已经发生了一个特定的事情。
- 强迫进程执行它自己代码中的信号处理程序。
信号的特点:简单,不能携带大量信息,满足某个特定条件才发送,优先级比较高。
通过kill -l
命令可以查看系统的信号。其中1 ~ 31是常规信号,34 ~ 64是当前预定好待使用的信号。标红的需要掌握。
查看信号的详细信息:man 7 signal
,有些信号有3个编号,实际上对应不同的系统平台。其中中间一列是x86,arm架构。
信号的 5 种默认处理动作
- Term 终止进程
- Ign 当前进程忽略掉这个信号
- Core 终止进程,并生成一个 Core 文件,保存进程退出的一些错误信息,以便我们对其进行调试
- Stop 暂停当前进程
- Cont 继续执行当前被暂停的进程
信号的几种状态:产生,未决,递达
1 | //可能按照视频操作产生不了core文件,这是因为ubuntu预装了apport自动报错警告的功能,有了这个就不会产生core文件了,想要输出core文件就要sudo service apport stop关闭apport |
信号相关的函数:
1 | int kill(pid_t pid, int sig); |
kill函数的实例程序:
1 | /* |
alarm函数:
1 | /* |
setitimer函数:
1 | /* |
信号捕捉函数:
1 | /* |
信号集相关函数:
许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t 。
在 PCB 中有两个非常重要的信号集。一个称之为 “阻塞信号集 ”,另一个称之为“未决信号集 。这两个信号集都是内核使用位图机制(二进制位来表示信号)来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。
信号的 “未决 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
信号的 “阻塞 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
PCB里包括文件描述符,进程PID,PPID等信息,也包括未决信号集,阻塞信号集的信息。
- 用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)
- 信号产生但是没有被处理 (未决)
- 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集) - SIGINT信号状态被存储在第二个标志位上 - 这个标志位的值为0, 说明信号不是未决状态 - 这个标志位的值为1, 说明信号处于未决状态
- 这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
- 阻塞信号集默认不阻塞任何的信号
- 如果想要阻塞某些信号需要用户调用系统的API
在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
- 如果没有阻塞,这个信号就被处理
- 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理
- 如果没有阻塞,这个信号就被处理
1 | //一些自定义的信号集相关的函数 |
一个例子:
1 | /* |
一些处理内核当中的信号集的函数:
1 | int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);//将自身定义的信号集设置到内核当中 |
前台进程和后台进程:前台进程运行时不能执行其他命令,后台进程执行时可以同时执行其他命令。./xxx &
程序从后台执行,这时输入fg
可以再次将程序切换到前台。
1 | /* |
sigaction函数:推荐使用,ubuntu下和signal一样。
1 | /* |
信号集的特点:
- 通过sigprocmask将自建信号集放到系统信号集后执行完操作后,系统信号集恢复为原来的样子。
- 当调用信号捕捉的回调函数时,如果这时候再出现此信号,会将此信号屏蔽掉,只有回调函数执行完之后再遇到才有用。
- 阻塞的常规信号不支持排队(1 ~ 31),后面的支持排队。未决信号集和阻塞信号集只能记录一个状态,其他的被丢弃。
SIGCHLD 信号产生的条件
- 子进程终止时
- 子进程接收到 SIGSTOP 信号停止时
- 子进程处在停止态,接受到 SIGCONT 后唤醒时
以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号,这个信号可以用来解决僵尸进程的问题。
1 | /* |
2.8 共享内存:
共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种 IPC (进程间通信)机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。
共享内存使用步骤:
- 调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
- 使用 shmat() 来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分,也就是将这个共享内存与进程关联起来。
- 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
- 调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
- 调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步,防止其他的进程使用这部分共享进程。
1 | //共享内存操作函数 |
共享内存实现进程间通信:
1 | //write_shm.c |
问题1:操作系统如何知道一块共享内存被多少个进程关联?
- 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch
- shm_nattach 记录了关联的进程个数
问题2:可不可以对共享内存进行多次删除 shmctl
可以;因为shmctl 标记删除共享内存,不是直接删除。
什么时候真正删除呢?,当和共享内存关联的进程数为0的时候,就真正被删除,当共享内存的key为0的时候,表示共享内存被标记删除了。
如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。
共享内存和内存映射的区别
1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
2.共享内存效果更高
3.内存,所有的进程操作的是同一块共享内存。内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
4.数据安全:进程突然退出,共享内存还存在;内存映射区消失;运行进程的电脑死机,宕机了;数据存在在共享内存中,没有了;内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。
5.生命周期
内存映射区:进程退出,内存映射区销毁
共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机。如果一个进程退出,会自动和共享内存进行取消关联。
常用的共享内存操作命令:
1 | //ipcs 用法 |
共享内存的键被标记为0说明要标记删除,等所有的连接到共享内存的进程都结束后删除。
2.9 守护进程
终端:
在 UNIX 系统中,用户通过终端登录系统后得到一个 shell 进程,这个终端成为 shell 进程的控制终端( Controlling Terminal ),进程中,控制终端是保存在 PCB 中的信息,而 fork() 会复制 PCB 中的信息,因此由shell 进程启动的其它进程的控制终端也是这个终端。
通过echo $$
可以查看当前进程的PID.
默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。
在控制终端输入一些特殊的控制键可以给前台进程发信号,例如 Ctrl + C 会产生 SIGINT 信号, Ctrl + 会产生 SIGQUIT 信号。
进程组:
- 进程组和会话在进程之间形成了一种两级层次关系:进程组是一组相关进程的集合,会话是一组相关进程组的集合。进程组和会话是为支持 shell 作业控制而定义的抽象概念,用户通过 shell 能够交互式地在前台或后台运行命令。
- 进行组由一个或多个共享同一进程组标识符( PGID )的进程组成。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程 ID 为该进程组的 ID ,新进程会继承其父进程所属的进程组 ID 。
- 进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。
会话:
- 会话是一组进程组的集合。会话首进程是创建该新会话的进程,其进程 ID 会成为会话 ID 。新进程会继承其父进程的会话 ID 。
- 一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。
- 在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。
- 当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程。
find / 2 > /dev/null | wc l &
指令find / 2
表示查找2,>
表示重定向到/dev/null
这个文件,| wc l
表示通过管道查看数量,创建一个子进程。&
表示后台运行。
sort < longlist | uniq c
在前台运行
会话的首进程是bash,bash中只能同时运行一个前台进程组。会话ID是SID
进程组、会话操作函数
1 | pid_t getpgrp(void); |
守护进程( Daemon Process ),也就是通常说的 Daemon 进程(精灵进程),是Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以 d 结尾的名字。
守护进程具备下列特征:
- 生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭。
- 它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如 SIGINT 、 SIGQUIT )。
Linux 的大多数服务器就是用守护进程实现的。比如, Internet 服务器 inetd,Web 服务器 httpd 等,远程登陆sshd。
守护进程的创建步骤
- 执行一个 fork(),之后父进程退出,否则等父进程退出时终端产生一个结束符。子进程继续执行,确保子进程PID和PGID不同,这样才能进行第二步setsid()。
- 子进程调用 setsid() 开启一个新会话,创建新会话与当前终端的会话不同,防止内核产生信号干扰守护进程。如果在父进程中,可能会产生相同的组ID和会话ID。
- 清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限。
- 修改进程的当前工作目录,通常会改为根目录(/),目的是防止守护进程被卸载。
- 关闭守护进程从其父进程继承而来的所有打0开着的文件描述符,防止守护进程在终端产生一些数据。
- 在关闭了文件描述符 0 、 1 、 2 之后,守护进程通常会打开 /dev/null 并使用 dup2()使所有这些描述符指向这个设备,所有的输入输出都会被丢弃掉。
- 核心业务逻辑
1 | /* |