C/C++Unix/Linux应用层编程基本原理

深入理解pthread互斥量与条件变量的使用

深入理解pthread互斥量与条件变量的使用

在多线程编程中,线程同步是一个关键问题。为了避免多个线程同时访问共享资源导致的数据竞争和不一致,POSIX线程库(pthread)提供了互斥量(Mutex)和条件变量(Condition Variable)两种重要的同步机制。本文将详细介绍它们的使用方法,并通过代码示例帮助读者更好地理解。

1. 互斥量(Mutex)

互斥量是一种简单的同步机制,用于保护共享资源,确保同一时间只有一个线程可以访问该资源。互斥量的基本操作包括初始化、加锁、解锁和销毁。

1.1 互斥量的初始化

互斥量可以通过静态或动态方式初始化。静态初始化使用宏PTHREAD_MUTEX_INITIALIZER,而动态初始化使用pthread_mutex_init函数。

// 静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 动态初始化
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
1.2 互斥量的加锁与解锁

线程在访问共享资源前需要加锁,访问完成后解锁。如果互斥量已被其他线程锁定,当前线程会被阻塞,直到互斥量被解锁。

pthread_mutex_lock(&mutex);
// 访问共享资源
pthread_mutex_unlock(&mutex);
1.3 互斥量的销毁

当互斥量不再需要时,应调用pthread_mutex_destroy函数销毁它,以释放系统资源。

pthread_mutex_destroy(&mutex);

2. 条件变量(Condition Variable)

条件变量用于生产者-消费者模型中,防止消费者拿到互斥量后的忙等待。它允许线程在某个条件未满足时进入等待状态并释放互斥锁,并在条件满足时被唤醒并请求互斥锁。条件变量通常与互斥量一起使用。

2.1 条件变量的初始化

条件变量也可以通过静态或动态方式初始化。

// 静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

// 动态初始化
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);
2.2 条件变量的等待与通知

线程在等待条件变量时,会释放持有的互斥量,并在条件满足时重新获得互斥量。其他线程可以通过pthread_cond_signalpthread_cond_broadcast唤醒等待的线程。

// 等待条件变量
pthread_mutex_lock(&mutex);
while (condition == false) { //一定要将循环检查共享资源,防止被唤醒后共享资源被没有被生产者生产
    pthread_cond_wait(&cond, &mutex);
}
pthread_mutex_unlock(&mutex);

// 通知条件变量
pthread_mutex_lock(&mutex);
condition = true;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
2.3 条件变量的销毁

当条件变量不再需要时,应调用pthread_cond_destroy函数销毁它。

pthread_cond_destroy(&cond);

3. 示例代码

以下是一个使用互斥量和条件变量的简单示例,展示了如何实现线程间的同步。

#include <pthread.h>
#include <stdio.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;

void *thread_function(void *arg) {
    pthread_mutex_lock(&mutex);
    while (ready == 0) {
        pthread_cond_wait(&cond, &mutex);
    }
    printf("Thread condition met, proceeding\n");
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t thread_id;
    pthread_create(&thread_id, NULL, thread_function, NULL);

    pthread_mutex_lock(&mutex);
    printf("Main thread signaling condition variable\n");
    ready = 1;
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);

    pthread_join(thread_id, NULL);
    return 0;
}

在这个示例中,主线程通过条件变量唤醒等待的线程,确保线程在条件满足时继续执行。

4. 总结

互斥量和条件变量是多线程编程中常用的同步机制。互斥量用于保护共享资源,而条件变量用于防止线程的忙等待,协调生产者与消费者线程。通过合理使用这两种机制,可以有效地避免数据竞争和死锁问题,提高多线程程序的稳定性和性能。

希望本文能帮助你更好地理解和使用pthread互斥量与条件变量。如果你有任何问题或需要进一步的帮助,请随时留言!

ncurses库的使用总结

ncurses库的使用总结


前言

ncurses(new curses)是一个程序库,它提供的API可以允许程序员编写独立于终端的基于文本的用户界面。它是一个虚拟终端中的“类GUI”应用软件工具箱。它还优化了屏幕刷新方法,以减少使用远程shell时遇到的延迟。


一、环境初始化函数

  1. WINDOW *initscr(void)
    参数:无
    返回值:返回标准窗口stdscr的指针;失败返回NULL
    作用:初始化终端进入curses模式,必须在其他ncurses函数前调用。
    使用注意:每个程序只能调用一次,需与endwin()配对。
  2. int endwin(void)
    参数:无
    返回值:成功返回OK,失败返回ERR
    作用:释放ncurses资源,恢复终端原始状态。需在程序退出前调用。

    // 示例:初始化与退出
    initscr();
    // ...其他操作...
    endwin();
  3. 信号处理(非函数原型,但关键)
    使用场景:防止程序异常终止导致终端显示异常。
    示例

    #include <signal.h>
    void sig_handler(int signo) { endwin(); exit(0); }
    signal(SIGINT, sig_handler);  // 处理Ctrl+C

二、输入模式控制

  1. int raw(void) / int cbreak(void)
    参数:无
    返回值:成功返回OK,失败返回ERR
    区别
    raw():禁用行缓冲和信号处理(如Ctrl+C会直接传递为输入字符)。
    cbreak():禁用行缓冲但保留信号处理。
  2. int echo(void) / int noecho(void)
    参数:无
    作用:控制输入回显,noecho()常用于密码输入等场景。
  3. int keypad(WINDOW *win, bool bf)
    参数
    win:目标窗口(通常为stdscr)。
    bfTRUE启用功能键(如方向键/F1-F12),FALSE禁用。
    返回值:成功返回OK,失败返回ERR
  4. int halfdelay(int tenths)
    参数tenths为等待输入的0.1秒倍数(1-255)。
    作用:设置输入超时,超时后返回ERR

三、屏幕输出与光标控制

  1. 基础输出函数
    int addch(chtype ch)
    参数ch为字符(可组合属性,如ch | A_BOLD)。
    int printw(const char *fmt, ...)
    参数:类似printf,支持格式化字符串。
    int addstr(const char *str)
    参数:直接输出完整字符串。
  2. 光标移动函数
    int move(int y, int x)
    参数:目标坐标(y, x)(行、列)。
    返回值:成功返回OK,失败返回ERR
    mvaddch(int y, int x, chtype ch)
    等效操作move(y, x); addch(ch);
  3. 屏幕刷新与清空
    int refresh(void):将stdscr的内容刷新到物理屏幕。
    int erase(void):清空stdscr内容,不重置光标。
    int clear(void):清屏并重置光标到(0,0)

四、窗口管理

  1. 窗口创建与销毁
    WINDOW *newwin(int nlines, int ncols, int y, int x)
    参数:窗口行数、列数、起始坐标(y, x)
    返回值:新窗口指针。
    int delwin(WINDOW *win)
    注意:需先销毁子窗口再销毁父窗口。
  2. 窗口操作
    int wrefresh(WINDOW *win):刷新指定窗口到屏幕。
    int box(WINDOW *win, chtype verch, chtype horch)
    参数verch为垂直边框字符,horch为水平边框字符。

五、输入处理

  1. 字符输入
    int getch(void)
    返回值:阻塞模式下等待输入,返回字符或功能键宏(如KEY_LEFT)。
    int nodelay(WINDOW *win, bool bf)
    参数bfTRUE时进入非阻塞模式(无输入返回ERR)。
  2. 坐标获取宏
    getyx(WINDOW *win, int y, int x)
    作用:获取窗口当前光标坐标(yx需为变量地址)。
    getmaxyx(WINDOW *win, int y, int x)
    作用:获取窗口最大行数和列数。

六、最佳实践示例

#include <ncurses.h>
#include <signal.h>

void sig_handler(int signo) { endwin(); exit(0); }

int main() {
    initscr();
    signal(SIGINT, sig_handler);
    cbreak();
    noecho();
    keypad(stdscr, TRUE);

    printw("Press F1 to exit");
    int ch = getch();
    if (ch == KEY_F(1)) {
        endwin();
        return 0;
    }

    endwin();
}

以上内容综合了多个文档来源,具体函数细节可参考官方手册或相关示例。

System V 消息队列总结

System V 消息队列总结:

前言

消息队列是一种面向消息的ipc机制,即内核会保证进程对消息队列IO时数据块(消息)的整体性。且其是双工的,任何进程都可以往里放消息,同时任何进程都可以往里读消息(甚至读到其自己的)。


1. msgget - 创建消息队列/获取已有消息队列的标识符

#include <sys/msg.h>

int msgget(key_t key, int msgflg);

参数
key:消息队列的唯一键值(通常由 ftok 生成),或使用 IPC_PRIVATE 创建私有队列。
msgflg:标志位(如 IPC_CREAT 创建队列,IPC_EXCL 配合 IPC_CREAT 确保队列不存在时新建)。
返回值:成功返回消息队列标识符(msqid),失败返回 -1


2. msgsnd - 发送消息

#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数
msqid:消息队列标识符。
msgp:指向消息结构体的指针(需自定义,见下文)。
msgsz:消息内容(mtext)的字节数(不包括 mtype 字段)。
msgflg:标志位(如 IPC_NOWAIT 非阻塞发送,队列满时立即返回错误)。
返回值:成功返回 0,失败返回 -1

注意:就算多进程/线程同时非阻塞地调用该函数,内核依旧会保证消息之间地独立性。因为内核并不是依靠其阻塞来保证独立性的。


3. msgrcv - 接收消息

#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

参数
msqid:消息队列标识符。
msgp:接收消息的缓冲区指针(需自定义消息结构体)。
msgsz:缓冲区中 mtext 部分的最大容量。
msgtyp:指定接收消息的类型(见下文规则)。
msgflg:标志位(如 IPC_NOWAIT 非阻塞接收,MSG_NOERROR 允许截断过长消息)。
返回值:成功返回实际接收的 mtext 字节数,失败返回 -1


4. msgctl - 控制消息队列

#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

参数
msqid:消息队列标识符。
cmd:控制命令(常用值):

◦ **`IPC_STAT`**:获取队列元数据到 `buf`。
◦ **`IPC_SET`**:通过 `buf` 修改队列元数据(如权限、容量限制)。
◦ **`IPC_RMID`**:立即删除队列(`buf` 可设为 `NULL`)。

buf:指向 struct msqid_ds 的指针(用于读写元数据)。
返回值:成功返回 0,失败返回 -1


关键数据结构

消息结构体(需自定义)

struct msgbuf {
    long mtype;     // 消息类型(必须 > 0)
    char mtext[];  // 消息内容(实际长度由 `msgsz` 指定,这里的字符数组表示就是一块内存,可以是各种数据的组合)
};

队列元数据结构体(struct msqid_ds

提取自内核维护的msg_ids数据结构的对应entries指针指向的结构。

struct msqid_ds {
    struct ipc_perm msg_perm;  // 权限信息(所有者、读写权限等)
    time_t          msg_stime; // 最后发送时间
    time_t          msg_rtime; // 最后接收时间
    time_t          msg_ctime; // 最后修改时间
    unsigned long   msg_cbytes; // 当前队列中的字节数
    msgqnum_t       msg_qnum;   // 当前队列中的消息数
    msglen_t        msg_qbytes; // 队列最大容量(字节数)
    pid_t           msg_lspid;  // 最后发送消息的进程PID
    pid_t           msg_lrpid;  // 最后接收消息的进程PID
};

核心原理

  1. 消息类型(mtype
    • 用于分类消息,接收时可指定类型过滤(如 msgtyp > 0 匹配特定类型,msgtyp = 0 接收任意类型,msgtyp < 0 接受mtype小于msgtyp绝对值的mtype最小的消息)。
    • 类型为 long 类型,必须为正整数。
  2. 内核管理
    • 消息队列以内核对象形式存在,独立于进程生命周期(需显式调用 msgctl(IPC_RMID) 删除)。
    • 消息以链表形式存储,保证先进先出(FIFO),但可通过 msgtyp 实现优先级读取。
  3. 同步与原子性
    msgsndmsgrcv 是原子操作:单次调用发送或接收一条完整消息。
    • 若队列满或空,默认阻塞进程(除非指定 IPC_NOWAIT)。

示例代码片段

// 定义消息结构体(实际使用需动态分配 mtext 大小)
struct my_msg {
    long mtype;
    char mtext[100];
};

// 发送消息
struct my_msg msg_send = {1, "Hello"};
msgsnd(msqid, &msg_send, strlen(msg_send.mtext), 0);

// 接收消息
struct my_msg msg_recv;
msgrcv(msqid, &msg_recv, sizeof(msg_recv.mtext), 1, 0);

总结

适用场景:需要类型过滤、原子性操作或异步通信的进程间交互(如任务分发、事件通知)。
缺点:依赖内核资源,需手动管理队列生命周期;跨平台支持较弱(推荐优先考虑 POSIX 消息队列或套接字)。

C语言可变参数与命令行参数解析:stdarg与getopt详解

C语言可变参数与命令行参数解析:stdarg与getopt详解


一、可变参数处理:stdarg.h

1.1 核心功能

stdarg.h 头文件提供在函数中处理不定数量参数的能力,常用于实现类似printf()的格式化输出函数。通过宏定义实现,与平台特性强关联。

1.2 核心类型与函数原型

#include <stdarg.h>

// 参数列表指针类型
va_list;

// 初始化va_list指针,last_param是最后一个固定参数名
void va_start(va_list ap, last_param);

// 获取下一个参数,type为参数类型(如int、char*)
// 一定要确定可变参数的个数,不要在最后一个参数被读取了依然调用该函数
type va_arg(va_list ap, type);

// 清理va_list指针
void va_end(va_list ap);

1.3 使用步骤

  1. 函数声明使用省略号(如int func(int n, ...))
  2. 定义va_list变量
  3. va_start初始化指针
  4. va_arg逐个读取参数
  5. va_end释放资源

1.4 示例:简化版printf函数实现

#include <stdarg.h>
#include <stdio.h>

void my_printf(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);  // 初始化参数列表

    for (const char *p = fmt; *p != '\0'; p++) {
        if (*p != '%') {
            putchar(*p);  // 普通字符直接输出
            continue;
        }

        // 处理格式符 %
        switch (*++p) {    // 跳过%并读取下一个字符
            case 'd': {    // 处理整数
                int num = va_arg(ap, int);
                printf("%d", num);  // 借用标准库输出
                break;
            }
            case 's': {    // 处理字符串
                char *str = va_arg(ap, char*);
                printf("%s", str);
                break;
            }
            case 'c': {    // 处理字符
                char ch = va_arg(ap, int);  // char提升为int传递
                putchar(ch);
                break;
            }
            case '%': {    // 转义%符号
                putchar('%');
                break;
            }
            default: {     // 未知格式符
                putchar('?');
                break;
            }
        }
    }

    va_end(ap);  // 清理参数列表
}

// 调用示例:
// my_printf("ID:%d, Name:%s, Level:%c%%", 101, "Alice", 'A');
// 输出: ID:101, Name:Alice, Level:A%

1.5 注意事项

必须至少包含一个固定参数
va_arg必须按实际类型调用(类型错误导致未定义行为)
• C99后支持va_copy复制参数列表


二、命令行参数解析:getopt()

2.1 核心功能

unistd.h中的getopt()函数提供命令行选项解析能力,支持带参数选项(如-f filename)、组合短选项(如-abc)等常见格式。

2.2 函数原型与全局变量

#include <unistd.h>

int getopt(int argc, char *const argv[],
           const char *optstring);

// 全局变量:
extern char *optarg;  // 当前选项的参数值
extern int optind;    // 下一个要处理的argv索引,在getopt返回-1时其值为第一个非选项或选项参数的索引
extern int optopt;    // 无效选项字符

选项字符串格式:

单个字符:表示无参数选项(如"a"对应-a
字符后接冒号:必须带参数(如"f:"对应-f file
字符后接双冒号:可选参数(非标准扩展,谨慎使用)

2.3 使用流程

int main(int argc, char **argv) {
    int opt;
    while ((opt = getopt(argc, argv, "hf:v")) != -1) {
        switch (opt) {
            case 'h':
                printf("Help info\n");
                break;
            case 'f':
                printf("File: %s\n", optarg); // 获取参数
                break;
            case 'v':
                printf("Version 1.0\n");
                break;
            case '?': // 未知选项
                printf("Unknown option: %c\n", optopt);
                break;
        }
    }
    // 处理剩余参数(非选项参数)
    for (int i = optind; i < argc; i++) {
        printf("Extra argument: %s\n", argv[i]);
    }
    return 0;
}

2.4 运行示例

$ ./demo -f config.txt -v input.txt
File: config.txt
Version 1.0
Extra argument: input.txt

2.5 注意事项

• 选项参数通过optarg获取
• 遇到非选项参数时停止解析(可用--强制结束选项解析)
• 重复调用getopt()继续解析时需重置optind = 1


三、总结对比

特性stdarggetopt
主要用途函数内部处理可变参数解析命令行选项
核心操作va_start/va_arg/va_end宏getopt()函数循环调用
典型场景自定义格式化输出、数学计算开发命令行工具
参数类型安全无(依赖开发者保证)选项字符预先定义
跨平台性标准C库支持POSIX系统支持(Windows需额外实现)

掌握这两个工具,能够显著提升C语言函数设计的灵活性和命令行工具的开发效率。实际使用时需注意参数类型匹配与错误处理,避免因类型错误导致程序崩溃。

对底层IO的深度总结

对底层IO的深度总结

前言

本文介绍 stdio 库函数与系统调用 read/write 的核心行为,重点解析缓冲机制对 I/O 操作的影响。


一、read 系统调用

功能:原子地从内核缓冲区(内存)读取数据块到用户缓冲区(内存)。原子——多进程同时调用read也会保证每个进程读取到的数据块是完整的。

具体表格:

I/O类型是否启用O_NONBLOCK数据/状态条件read行为
标准输入输入未就绪(如终端无输入)阻塞,直到输入数据或EOF
标准输入输入未就绪立即返回EAGAIN错误
标准输入无所谓输入已就绪(用户键入回车读取实际输入字节数(≤请求字节数)
标准输入无所谓输入结束(如Ctrl+D)返回0(EOF)
普通文件无所谓文件未结束读取请求的字节数(除非文件剩余字节不足,返回实际剩余字节数)
普通文件无所谓已到文件结尾返回0(EOF)
管道/FIFO管道中无数据,但写入端未关闭阻塞,直到有数据写入
管道/FIFO管道中无数据,且写入端已关闭返回0(EOF)
管道/FIFO管道中有数据(≤请求字节数)读取实际可用字节数
管道/FIFO管道中无数据,但写入端未关闭立即返回EAGAIN错误
管道/FIFO管道中有数据(≤请求字节数)读取实际可用字节数
套接字无数据到达(面向连接,如TCP)阻塞,直到数据到达或连接关闭
套接字连接关闭(如TCP收到FIN)返回0(EOF)
套接字数据到达(≤请求字节数)读取实际到达字节数
套接字无数据到达立即返回EAGAIN错误
套接字数据到达(≤请求字节数)读取实际到达字节数

总结
read 何时阻塞:

  • 1.未设置O_NONBLOCK,
  • 2.对象是支持阻塞的设备(终端设备、套接字、管道等)而非普通文件,
  • 3.此时内核缓冲区恰好没有数据。

阻塞需同时满足上述三种情况。可以理解为read会等待可能提供数据的设备。其余情况,内核缓冲区一旦有数据块被write写入就一次性搬走并返回数据量没读到就返回0(EOF/设备关闭)或-1(发生错误)


二、write 系统调用

功能:原子地将用户缓冲区(内存)的数据块写入内核缓冲区(内存)。原子——多进程同时调用write也能保证每个进程写的数据块是独立的。

对象类型是否启用 O_NONBLOCK行为描述
标准输出未启用如果标准输出是终端设备,write 会阻塞,直到数据被读取或终端缓冲区满。
启用如果标准输出是终端设备,write 会立即返回。如果终端缓冲区满,返回 -1 并设置 errnoEAGAIN
普通文件未启用write 会立即将数据写入文件,不会阻塞。
启用write 会立即将数据写入文件,不会阻塞。
管道/FIFO未启用如果写入字节数 nPIPE_BUFwrite 会原子地写入n字节;如果 n > PIPE_BUFwrite不能保证原子性。
启用如果写入字节数 nPIPE_BUFwrite 会原子地写入n字节;如果 n > PIPE_BUF,返回 -1 并设置 errnoEAGAIN
套接字未启用如果套接字缓冲区有足够空间,write 会立即写入;否则会阻塞,直到有足够空间。
启用如果套接字缓冲区有足够空间,write 会立即写入;否则返回 -1 并设置 errnoEAGAIN
读取端关闭未启用如果写入对象是管道、FIFO 或套接字,且读取端关闭,write 会返回 -1 并设置 errnoEPIPE,同时可能会产生 SIGPIPE 信号。
启用如果写入对象是管道、FIFO 或套接字,且读取端关闭,write 会返回 -1 并设置 errnoEPIPE,同时可能会产生 SIGPIPE 信号。

关键点

  • write 返回仅表示数据块已提交给内核,不保证已写入磁盘。
  • 阻塞模式下,若内核缓冲区满,write 会阻塞;非阻塞模式直接返回 EAGAIN

三、stdio 库的缓冲机制

1. 输入类函数(如 scanf

  • 何时调用 read:当用户缓冲区无足够数据时触发 read
  • 缓冲影响

    • 若有用户缓冲区:read 填充库缓冲区,数据按需提取(如 scanf 遇空格/换行停止)。
    • 若无缓冲区(_IONBF):read 直接填充用户指定内存。

示例对比

// 例1:关闭缓冲区(_IONBF)
setvbuf(stdin, NULL, _IONBF, 0);
scanf("%s", string);  // read直接填充string,"world"留在内核缓冲区
// 例2:启用缓冲区(默认)
scanf("%s", string);  // read填充库缓冲区,"world"保留在库缓冲区中

2. 输出类函数(如 printf

  • 何时调用 write:由缓冲模式决定:

    缓冲模式触发条件
    无缓冲(_IONBF立即调用 write
    行缓冲(_IOLBF遇到换行符或缓冲区满
    全缓冲(_IOFBF缓冲区满或手动刷新(fflush

示例对比

// 行缓冲(默认)
printf("I'm A.\n");  // 遇换行符立即调用write
write(STDOUT_FILENO, "I'm B.\n", 7);  // 直接输出
// 输出顺序:A → B
// 全缓冲(_IOFBF)
printf("I'm A.\n");  // 缓冲区未满,不调用write
write(STDOUT_FILENO, "I'm B.\n", 7);  // 直接输出
// 输出顺序:B → A(程序退出时刷新缓冲区)

四、close的机制

对于进程打开一个文件而言,有三个关联的数据结构:进程层面的文件描述符表,内核层面的打开文件表,文件系统层面的inode表
进程层面的文件描述符表记录了fd与fd指向的内核的打开文件表的文件描述(句柄);
内核层面的打开文件表的每个表项(即文件句柄)记录了当前文件IO操作的偏移量,以及文件在文件系统inode表中的位置(下标);
文件系统层面的inode表记录了每个文件的元数据,以及记录锁(系统调用fcntl()维护的POSIX锁)。

close(int fd)的作用就是断开文件描述符fd与文件描述(内核维护的打开文件表的表项)的指向关系,并在进程层面的文件描述符表删除这一表项。而内核层面的打开文件表的表项只有没有任何进程的文件描述符指向它的时候才会被删除。


总结

  • read/write 就是一个搬运者,可阻塞,也可立即返回。
  • stdio 封装了read与write,为用户进程建立缓冲区:

    • 输入类函数:缓冲决定数据暂存位置(库缓冲区或用户内存)。
    • 输出类函数:缓冲决定 write 的触发条件(换行符、缓冲区满或无缓冲)。

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

前言

对创建了进程组的进程成为进程组首进程,其特点是其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信号,但是后台进程组没有收到信号。此时这些信号都是内核发送的

This is just a placeholder img.