• 首页
  • 搜索
  • 工具
  • 分类
  • 日志
  • 友链
  • 图片

It's Geek KingYoungy

KEEP CHALLENGE
C/C++Unix/Linux应用层程序设计

shell作业控制的两个问题:组长叛变与SIGHUP信号

2025-03-03 浏览量 361 暂无评论

前言

对创建了进程组的进程成为进程组首进程,其特点是其pid等于进程组的pgid。
对创建了会话的进程称为会话首进程,该进程一般为shell进程,同时其本身又是一个进程组的首进程。

这首先引出第一个问题:在Linux系统中,为什么进程组首进程(包括会话首进程)既不能以其自身创建新进程组同时作为两个进程组的组长,又不能脱离原会话创建新会话呢?

其次,在用户认为作业结束,断开终端(比如关闭ssh窗口),系统究竟给哪个进程发送SIGHUP信号?

这两个问题涉及到linux内核的做法。


问题1:组长为什么不能叛变?

首先:方便应用程序判断进程是否是首进程。

内核维护会话与进程组严格的层次关系(进程组的组长一定代表其所在的进程组、进程组之间互斥组成会话)。这导致应用程序判断一个进程是否为进程组或会话首进程的唯一方式是比对pid、pgid与sid。不可能一个进程组的一个成员是另外一个进程组的组长(一个进程组包含另一个进程组)。同理,不可能一个会话包含另一个会话。

其次:保证作业控制的条理清晰

进程组之间不互斥的相互包含的关系会导致作业控制非常复杂, 例如:用户希望通过shell停止一个任务(进程组),但该任务下居然包含一个子任务(子进程组), shell不知道发信号的对象究竟包不包含该子任务。与linux的设计哲学相悖。

总结:保证结构清晰(树形结构)、不复杂。

补充:

进程组、会话是作业控制的单位,
而父子进程的关系是wait的基础。
两者独立互不干扰。


问题2:谁会收到SIGHUP信号?

下述程序一个为进程本身与其子进程建立了SIGHUP的信号处理器,另一个只是为子进程建立SIGHUP的处理器。这两个程序将代替shell本身运行的程序,拥有shell的会话首进程与控制终端的控制进程的双重身份。

程序:

#define _GNU_SOURCE     /* Get strsignal() declaration from <string.h> */
#include <string.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

_Noreturn void errExit(const char *p){
    perror(p);
    exit(EXIT_FAILURE);
}

static void             /* Handler for SIGHUP */
handler(int sig)
{
    printf("PID %ld: caught signal %2d (%s)\n", (long) getpid(),
            sig, strsignal(sig));
                        /* UNSAFE (see Section 21.1.2) */
}

int
main(int argc, char *argv[])
{
    pid_t parentPid, childPid;
    int j;
    struct sigaction sa;

    if (argc < 2 || strcmp(argv[1], "--help") == 0){
        fprintf(stderr, "%s {d|s}... [ > sig.log 2>&1 ]\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    setbuf(stdout, NULL);               /* Make stdout unbuffered */

    parentPid = getpid();
    printf("PID of parent process is:       %ld\n", (long) parentPid);
    printf("Foreground process group ID is: %ld\n",
            (long) tcgetpgrp(STDIN_FILENO));

    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sa.sa_handler = handler;
    if (sigaction(SIGHUP, &sa, NULL) == -1)
        errExit("sigaction");

    for (j = 1; j < argc; j++) {        /* Create child processes */
        childPid = fork();
        if (childPid == -1)
            errExit("fork");

        if (childPid == 0) {            /* If child... */
            if (argv[j][0] == 'd')      /* 'd' --> to different pgrp */
                if (setpgid(0, 0) == -1)
                    errExit("setpgid");

            sigemptyset(&sa.sa_mask);
            sa.sa_flags = 0;
            sa.sa_handler = handler;
            if (sigaction(SIGHUP, &sa, NULL) == -1)
                errExit("sigaction");
            break;                      /* Child exits loop */
        }
    }

    /* All processes fall through to here */

    alarm(60);      /* Ensure each process eventually terminates */

    printf("PID=%ld PGID=%ld\n", (long) getpid(), (long) getpgrp());
    for (;;)
        pause();        /* Wait for signals */
}

另一个程序除了不为父进程建立处理器之外,与其一模一样。

结果

为父进程建立处理器的程序,通过

$ exec ./程序名 s s d d > log 2>&1

运行后关闭终端,会发现log中输出如下内容

PID of parent process is:       5133
Foreground process group ID is: 5133
PID=6962 PGID=5133
PID=6963 PGID=5133
PID=6964 PGID=6964
PID=5133 PGID=5133
PID=6965 PGID=6965
PID 5133: caught signal  1 (Hangup)

可以发现,只有父进程收到了内核发来的SIGHUP信号。这是由于,当会话首进程(即终端的控制进程)为SIGHUP建立了处理器,内核便认为该会话首进程有能力处理该信号(bash等shell的做法是将SIGHUP信号发送给会话的所有进程组以通知终端断开),此程序使用exec成为了会话首进程,也建立了信号处理器,但信号处理器并没有发送SIGHUP给会话中的其余进程,内核也不会发送,那么自然而然只有该进程才能收到SIGHUP信号了。
而不为父进程建立处理器的程序,输出结果如下:

PID of parent process is:       9994
Foreground process group ID is: 9994
PID=10131 PGID=9994
PID=10132 PGID=9994
PID=9994 PGID=9994
PID=10134 PGID=10134
PID=10133 PGID=10133
PID 10132: caught signal  1 (Hangup)
PID 10131: caught signal  1 (Hangup)

可见父进程被SIGHUP终止后,其同一进程组的子进程亦收到了SIGHUP信号,但是后台进程组没有收到信号。此时这些信号都是内核发送的。

- 阅读全文 -
C/C++Unix/Linux应用层程序设计

库、链接与执行

2025-03-02 浏览量 366 暂无评论

本文将介绍三个部分:可执行文件的编译与运行,静态库的创建以及动态库的创建, 在每个部分中将阐述静态链接器、动态链接器以及Linux内核在三个部分中各自发挥的作用。**


一、可执行文件的编译与运行

概括为:
一个有依赖的程序/动态库 = 嵌入静态库代码 + 嵌入动态库名字 + 可选择的嵌入动态库路径(倘若动态库不在标准路径)

1. 可执行文件的诞生:编译阶段

调用 gcc 可直接生成可执行文件:

gcc -o program.out main.c utils.c (动态库文件) -l库名 -L库路径以通知静态链接器如何找到该库

动态库可无需-l选项,直接与源文件放在一起。而静态库不行。
在此期间,gcc 完成了以下工作:

  1. 编译源码:将每个 .c 源文件编译成 .o 目标文件(实际由 cc1 编译器完成)。其中编译器找头文件的路径为本地/标准路径(/include,usr/include,usr/local/include...)
  2. 调用静态链接器 ld:将 所有.o 目标文件与静态库中main需要的目标文件链接成最终的可执行文件,并在可执行文件写下其依赖的动态库(如 libc.so)。其中ld要么在标准路径下找库文件,即/lib、/usr/lib等,可通过以下命令查看:
$ ld --verbose | grep SEARCH_DIR
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");

,但若指定了-L选项,则优先在该选项指定的路径下找。
如需要将来运行时提示动态链接器所依赖的动态库的地址,可以让ld在程序中写下运行时需要的动态库的地址,供动态链接器查找,即指定选项-Wl,-rpath,/动态库地址/。一些开箱即用的程序(程序与动态库打包在一起的应用)就是利用了这一特性,将动态库地址指定为'$ORIGIN'/lib,通知动态链接器该程序需要的共享库在此程序所在目录下的lib目录下。

最终生成的 program.out 中:

  • 包含所有 .o 目标文件的代码和静态链接的符号
  • 记录动态库依赖信息(如 libc.so.6),但不会嵌入动态库的代码(即只是记录动态库的名字)

2. 可执行文件的运行

通过 ./program.out 运行程序时:

  1. 内核加载可执行文件

    • 检查文件头是否为 ELF 格式
    • 若需要动态链接,内核加载动态链接器(如 /lib64/ld-linux-x86-64.so.2)
  2. 动态链接器工作流程
    动态链接器会:

    • 解析可执行文件的动态段(.dynamic),获取依赖的共享库列表
    • 按优先级搜索共享库:

      RPATH → LD_LIBRARY_PATH → /etc/ld.so.cache → /lib → /usr/lib

      请注意:ld.so.cache由ldconfig程序所维护,ldconfig是引导动态链接器并维护更新三方库的强力工具。
      让ldconfig监视除了标准路径外的其他路径的方法:
      在/etc/ld.so.conf.d目录下,新建一个.conf配置文件在其中输入你想要让其监视的目录的绝对路径即可,每条路径间换行分隔。
      让ldconfig根据其监视的目录下的变化,如新增了一个指定了soname的库,更新ld.so.cache以让动态链接器找到该库,则可以调用以下命令:

      ldconfig -v | grep 你的库名
    • 加载共享库到内存并完成符号重定位
    • 执行共享库的初始化代码(如构造函数)
    • 将控制权交给程序的 main 函数

二、静态库的创建

  1. 编译源文件生成目标文件:

    gcc -c utils1.c utils2.c -o utils1.o utils2.c
  2. 打包静态库:

    ar r libutils.a utils1.o utils2.o

    生成 libutils.a,本质是 .o 文件的集合


三、动态库的创建

  1. 编译位置无关代码:

    gcc -c -fPIC utils.c -o utils.o

    -fPIC 生成地址无关代码(该代码会根据进程的不同在不同内存偏移量处产生变量),确保库可被多个进程共享

  2. 生成共享库:

    gcc -shared -o libutils.so utils.o

与静态库一样,还是打包目标文件放在一起。但静态库使用的打包工具是ar,而动态库的打包直接可以用gcc编译器。
动态库的版本必须命名如下:libxxx.so.a.b.c,这里不解释。
如希望默认使用版本a下的最新版本,需指定其soname,即使用gcc的选项-Wl,-soname,/别名/,soname去掉b.c,且会被ldconfig用来创建一个符号链接,指向版本a下的最新版库。此时便可使用-l:libxxx.so.a链接版本a下的最新次要版本库。
如希望默认使用该库所有版本的最新版,则需要手动创建一个名为libxxx.so符号链接指向最新版的共享库。此时该库名称被称为链接器名称。


四、核心组件职责总结

组件作用阶段核心功能
静态链接器 ld编译阶段合并目标文件、解析静态符号、生成可执行文件或动态库
动态链接器 ld-linux.so运行时阶段加载共享库、动态符号解析、地址重定位、管理延迟绑定
Linux内核加载阶段验证ELF格式、加载可执行文件到内存、初始化用户态栈、调用动态链接器

关键区别总结

特性静态链接动态链接
库代码存储嵌入可执行文件独立存储在 .so 文件
内存占用每个进程独立加载多个进程共享同一份库内存
更新维护需重新编译程序替换 .so 文件即可生效
启动速度较快(无运行时加载开销)较慢(需动态链接)
磁盘空间较大较小
发生阶段可执行文件生成阶段运行阶段
任务完成者静态链接器ld(被gcc调用)动态链接器ld-linux.so

动静态库即打包的目标文件,通过编译器调用的静态链接器与用户源代码静态链接(合并代码,嵌入共享库名字)即可得到可执行文件。可执行文件需要运行时再动态链接共享库。
可执行文件 = 编译 + 静态链接
库 = 编译 + 打包
通过理解这些机制,开发者可以更好地优化程序架构,在模块化、性能和部署灵活性之间做出平衡。

- 阅读全文 -

欢迎使用 Typecho

2025-03-02 浏览量 360 评论数 1

如果您看到这篇文章,表示您的 blog 已经安装成功.
表格的每一行后面不要有空格哟

- 阅读全文 -
  1. 1
  2. ...
  3. 8
  4. 9
  5. 10
  • 站点概览
author

39 日志
4 分类
Creative Commons
  • 热门文章
  • 热评文章
  • 随机文章
  • 在 Debian 服务器上部署 FileBrowser 并集成到现有博客路径
  • 高等数学重要定理总结
  • 高等数学重要定义整理
  • C语言原子量的使用
  • 高等数学刷题心得
  • 欢迎使用 Typecho
  • 对底层IO的深度总结
  • 数据结构——树
  • 库、链接与执行
  • shell作业控制的两个问题:组长叛变与SIGHUP信号
  • C语言结构体/共用体的赋值限制
  • Linux Daemon进程开发指南:从创建到日志管理
  • 深入理解pthread互斥量与条件变量的使用
  • C语言可变参数与命令行参数解析:stdarg与getopt详解
  • 高等数学刷题心得

浏览量 : 10919

© 2025 It's Geek KingYoungy. Power By Typecho . Theme by Shiyi

浙ICP备2025160639号  |  浙公网安备33020502001222号

This is just a placeholder img.