主要探讨四个主题:信号处理、守护进程、进程组和会话。这些都是Linux/Unix进程管理中的重要概念。下面将分别深入探讨它们的工作原理和机制。
一、信号处理(Signal)
1.1 信号的基本概念
信号是Linux系统中用于进程间通信的一种机制,它是一种异步的通知,用来通知进程发生了某个事件。信号可以由内核、其他进程或进程自身发送。
常见信号列表:
信号编号 信号名 默认动作 说明
1 SIGHUP 终止 终端挂起或控制进程终止
2 SIGINT 终止 来自键盘的中断(Ctrl+C)
3 SIGQUIT 终止+核心转储 来自键盘的退出(Ctrl+\)
9 SIGKILL 终止 强制终止信号,不可捕获或忽略
10 SIGUSR1 终止 用户自定义信号1
12 SIGUSR2 终止 用户自定义信号2
14 SIGALRM 终止 由alarm函数设置的定时器超时
15 SIGTERM 终止 终止信号,可被捕获
17 SIGCHLD 忽略 子进程状态改变
20 SIGTSTP 停止 来自键盘的停止(Ctrl+Z)
1.2 信号的处理机制
当信号发生时,内核会在进程的进程表项中设置一个标志位,这个过程称为“递送”。信号产生和递送之间的时间间隔内,信号是“未决”的。
进程可以选择以下三种方式处理信号:
忽略信号:除了SIGKILL和SIGSTOP,其他信号都可以被忽略。
捕捉信号:为信号指定一个处理函数,当信号发生时,执行该处理函数。
执行默认动作:大多数信号的默认动作是终止进程。
1.3 信号处理函数
使用sigaction函数可以设置信号处理方式。示例:
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void signal_handler(int signo) {
if (signo == SIGINT) {
printf(“Received SIGINT\n”);
} else if (signo == SIGTERM) {
printf(“Received SIGTERM\n”);
}
}
int main() {
struct sigaction sa;
sa.sa_handler = signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; // 注册信号处理函数 if (sigaction(SIGINT, &sa, NULL) == -1) { perror("sigaction"); return 1; } if (sigaction(SIGTERM, &sa, NULL) == -1) { perror("sigaction"); return 1; } // 无限循环,等待信号 while (1) { pause(); } return 0;}
1.4 信号的阻塞与未决
进程可以阻塞信号,被阻塞的信号将保持在未决状态,直到进程解除阻塞。使用sigprocmask可以设置进程的信号屏蔽字。
c
#include <signal.h>
int main() {
sigset_t newset, oldset;
// 初始化信号集 sigemptyset(&newset); sigaddset(&newset, SIGINT); // 阻塞SIGINT // 设置信号屏蔽字 sigprocmask(SIG_BLOCK, &newset, &oldset); // 在这段代码中,SIGINT被阻塞 // 即使按下Ctrl+C,也不会中断 // 恢复原来的信号屏蔽字 sigprocmask(SIG_SETMASK, &oldset, NULL); return 0;}
二、守护进程(Daemon)
2.1 守护进程的特点
守护进程是运行在后台的一种特殊进程,它独立于控制终端,通常周期性地执行某种任务或等待处理某些事件。守护进程通常在系统启动时开始运行,直到系统关闭。
守护进程的特点:
在后台运行
脱离控制终端,避免被终端信号打断
独立于用户会话,通常以root权限运行
通常用来提供服务
2.2 编写守护进程的步骤
创建子进程,父进程退出:使子进程成为孤儿进程,被init进程收养。
在子进程中创建新会话:调用setsid,使子进程成为会话组长,脱离终端。
改变当前工作目录:通常改为根目录,避免占用可卸载的文件系统。
重设文件权限掩码:umask(0),确保守护进程创建文件时有足够的权限。
关闭不需要的文件描述符:释放从父进程继承的打开文件。
处理信号:通常忽略某些信号,如SIGHUP。
2.3 守护进程示例
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void daemonize() {
pid_t pid;
// 1. 创建子进程,父进程退出 pid = fork(); if (pid < 0) { perror("fork"); exit(1); } else if (pid > 0) { exit(0); // 父进程退出 } // 2. 在子进程中创建新会话 if (setsid() < 0) { perror("setsid"); exit(1); } // 3. 改变当前工作目录为根目录 if (chdir("/") < 0) { perror("chdir"); exit(1); } // 4. 重设文件权限掩码 umask(0); // 5. 关闭标准输入、输出、错误输出 close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); // 6. 处理信号 signal(SIGCHLD, SIG_IGN); signal(SIGHUP, SIG_IGN);}
int main() {
daemonize();
// 守护进程的具体工作 while (1) { // 在这里执行守护进程的任务 // 例如,每隔一段时间执行某个操作 sleep(10); } return 0;}
三、进程组和会话
3.1 进程组(Process Group)
进程组是一个或多个进程的集合,每个进程组有一个唯一的进程组ID。进程组中的进程通常与同一个作业(job)相关联,可以接收相同的信号。
每个进程属于一个进程组。
进程组ID等于组长的进程ID。
组长进程可以创建新的进程组,并将其他进程加入该组。
3.2 会话(Session)
会话是一个或多个进程组的集合,通常由一次登录形成一个会话。每个会话有一个唯一的会话ID,并且有一个会话首进程(session leader),即创建会话的进程。
每个会话可以有一个控制终端(controlling terminal)。
与控制终端建立连接的会话首进程称为控制进程(controlling process)。
一个会话中的进程组可以分为一个前台进程组和多个后台进程组。
3.3 相关函数
setsid():创建一个新会话,并成为会话首进程和进程组长,同时脱离控制终端。
setpgid(pid_t pid, pid_t pgid):将指定进程加入指定进程组,或创建新的进程组。
getpgrp():返回当前进程的进程组ID。
getsid(pid_t pid):返回指定进程的会话ID。
3.4 示例:创建新会话和进程组
c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid;
// 创建子进程 pid = fork(); if (pid < 0) { perror("fork"); return 1; } else if (pid == 0) { // 子进程中创建新会话 if (setsid() < 0) { perror("setsid"); return 1; } printf("Child: PID=%d, PGID=%d, SID=%d\n", getpid(), getpgrp(), getsid(0)); // 子进程的工作 sleep(10); } else { // 父进程 printf("Parent: PID=%d, PGID=%d, SID=%d\n", getpid(), getpgrp(), getsid(0)); sleep(5); } return 0;}
四、深入探讨工作原理和机制
4.1 信号处理的底层机制
当信号被发送给进程时,内核会在进程的进程控制块(PCB)中设置一个标志位。当进程从内核态返回到用户态时,内核会检查进程的未决信号,并调用相应的信号处理函数。信号处理函数在用户态执行,但内核为其设置了特殊的栈帧,使得信号处理函数返回后,进程可以继续原来的执行。
4.2 守护进程与系统服务
现代Linux系统使用systemd来管理守护进程。systemd提供了更强大的服务管理功能,包括依赖关系、并行启动、按需启动等。编写systemd服务需要创建一个.service文件,放在/etc/systemd/system/目录下。
4.3 进程组和会话与终端的关系
终端驱动程序会将输入和信号发送给前台进程组。当用户按下Ctrl+C时,终端驱动程序会向前台进程组发送SIGINT信号。同样,Ctrl+Z发送SIGTSTP信号。这使得用户可以通过终端控制作业的执行。
4.4 实际应用:实现一个简单的shell
一个简单的shell需要管理作业(jobs),包括前台作业和后台作业。这需要使用进程组和会话。当shell运行一个前台作业时,它会将作业放入一个新的进程组,并设置为前台进程组。当作业结束时,shell会重新获得控制权。
下面对进程信号处理、守护进程、进程组与会话:深度解析
一、信号处理(Signal)深度解析
1.1 信号的本质与分类
1.1.1 信号的完整生命周期
信号生命周期(从产生到处理): ┌─────────────────────────────────────────┐ │ 信号产生(Generation) │ │ 1. 硬件异常(除零、内存访问错误) │ │ 2. 软件事件(定时器到期、子进程终止) │ │ 3. 终端事件(Ctrl+C, Ctrl+Z) │ │ 4. 其他进程发送(kill, sigqueue) │ └───────────────────┬─────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ 信号递送(Delivery) │ │ 内核检查目标进程的信号屏蔽字 │ │ 如果信号被阻塞,标记为未决(Pending) │ │ 如果未被阻塞,立即递送 │ └───────────────────┬─────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ 信号处理(Handling) │ │ 1. 忽略(Ignore) │ │ 2. 捕获(Catch):执行用户处理函数 │ │ 3. 默认(Default):执行系统默认动作 │ └─────────────────────────────────────────┘ 关键状态: - 产生(Generated):信号已发生但未处理 - 未决(Pending):信号已产生但被阻塞 - 递送(Delivered):信号已发送给进程 - 捕获(Caught):信号被用户处理函数处理1.1.2 Linux信号分类表
// 标准信号(1-31)和实时信号(34-64)#define_NSIG65// 信号总数// 信号宏定义(部分)#defineSIGHUP1// 终端挂起或控制进程终止#defineSIGINT2// 键盘中断(Ctrl+C)#defineSIGQUIT3// 键盘退出(Ctrl+\)#defineSIGILL4// 非法指令#defineSIGTRAP5// 跟踪/断点陷阱#defineSIGABRT6// 异常终止#defineSIGBUS7// 总线错误#defineSIGFPE8// 浮点异常#defineSIGKILL9// 强制终止(不可捕获)#defineSIGUSR110// 用户自定义信号1#defineSIGSEGV11// 段错误#defineSIGUSR212// 用户自定义信号2#defineSIGPIPE13// 管道破裂#defineSIGALRM14// 定时器超时#defineSIGTERM15// 终止信号#defineSIGCHLD17// 子进程状态改变#defineSIGCONT18// 继续执行(如果停止)#defineSIGSTOP19// 停止进程(不可捕获)#defineSIGTSTP20// 终端停止信号(Ctrl+Z)#defineSIGTTIN21// 后台进程尝试读终端#defineSIGTTOU22// 后台进程尝试写终端#defineSIGURG23// 紧急数据到达socket#defineSIGXCPU24// CPU时间超限#defineSIGXFSZ25// 文件大小超限#defineSIGVTALRM26// 虚拟定时器超时#defineSIGPROF27// 性能分析定时器超时#defineSIGWINCH28// 窗口大小改变#defineSIGIO29// I/O就绪#defineSIGPWR30// 电源故障#defineSIGSYS31// 非法系统调用// 实时信号(34-64)#defineSIGRTMIN34#defineSIGRTMAX64// 信号标志#defineSA_NOCLDSTOP0x00000001// SIGCHLD不报告停止#defineSA_NOCLDWAIT0x00000002// 不创建僵尸进程#defineSA_SIGINFO0x00000004// 使用sa_sigaction而不是sa_handler#defineSA_ONSTACK0x08000000// 使用备用信号栈#defineSA_RESTART0x10000000// 系统调用自动重启#defineSA_NODEFER0x40000000// 不阻塞当前信号#defineSA_RESETHAND0x80000000// 处理一次后恢复默认1.2 信号处理的内核机制
1.2.1 内核中的信号数据结构
// Linux内核信号相关数据结构// task_struct中的信号相关字段structtask_struct{// ... 其他字段/* Signal handlers: */structsignal_struct*signal;structsighand_struct*sighand;sigset_tblocked;// 阻塞的信号集sigset_treal_blocked;structsigpendingpending;// 未决信号队列// ... 其他字段};// 信号描述符(每个进程共享)structsignal_struct{atomic_tcount;// 引用计数structsigpendingshared_pending;// 共享未决信号// 资源限制unsignedlongrlim[RLIM_NLIMITS];// 进程组和会话structpid*leader_pid;structtty_struct*tty;// 统计信息unsignedlonglongcutime,cstime;// 子进程用户/系统时间unsignedlonglongutime,stime;// 本进程用户/系统时间// 信号处理intgroup_exit_code;intgroup_stop_count;unsignedintflags;};// 信号处理描述符structsighand_struct{atomic_tcount;// 引用计数structk_sigactionaction[_NSIG];// 信号处理数组spinlock_tsiglock;// 保护信号处理wait_queue_head_tsignalfd_wqh;};// 未决信号队列structsigpending{structlist_headlist;// 未决信号链表sigset_tsignal;// 信号位图};// 内核信号动作structk_sigaction{structsigactionsa;#ifdef__ARCH_HAS_KA_RESTORER__sigrestore_t ka_restorer;#endif};// 用户空间sigaction结构structsigaction{union{__sighandler_t sa_handler;// 简单处理函数void(*sa_sigaction)(int,siginfo_t*,void*);// 扩展处理函数}__sigaction_handler;unsignedlongsa_flags;__sigrestore_t sa_restorer;sigset_tsa_mask;// 执行处理函数时阻塞的信号};1.2.2 信号递送的核心流程
// 内核信号递送核心代码(简化版)voidsignal_deliver(structtask_struct*tsk,structk_sigaction*ka,intsig,siginfo_t*info,sigset_t*oldset){structpt_regs*regs=task_pt_regs(tsk);// 1. 设置用户栈帧if(ka->sa.sa_flags&SA_SIGINFO){// 使用siginfo版本if(setup_rt_frame(sig,ka,info,oldset,regs)<0)return;}else{// 简单版本if(setup_frame(sig,ka,oldset,regs)<0)return;}// 2. 设置信号处理函数地址regs->ARM_ip=(unsignedlong)ka->sa.sa_handler;// 3. 设置返回地址(从信号处理返回后执行的地址)regs->ARM_lr=(unsignedlong)ka->sa.sa_restorer;// 4. 设置程序计数器指向信号处理函数regs->ARM_pc=regs->ARM_ip;// 5. 清除阻塞标志(除非指定SA_NODEFER)if(!(ka->sa.sa_flags&SA_NODEFER)){sigdelset(&tsk->blocked,sig);}// 6. 如果指定SA_ONESHOT,恢复默认处理if(ka->sa.sa_flags&SA_RESETHAND){ka->sa.sa_handler=SIG_DFL;}}// 设置信号栈帧(x86-64架构示例)intsetup_rt_frame(intsig,structk_sigaction*ka,siginfo_t*info,sigset_t*set,structpt_regs*regs){structrt_sigframe__user*frame;void__user*restorer;interr=0;// 在用户栈上分配帧空间frame=get_sigframe(ka,regs,sizeof(*frame));if(!access_ok(frame,sizeof(*frame)))return-EFAULT;// 设置ucontexterr|=__put_user(0,&frame->uc.uc_flags);err|=__put_user(NULL,&frame->uc.uc_link);err|=save_altstack(&frame->uc.uc_stack,regs->sp);// 保存寄存器状态err|=setup_sigcontext(&frame->uc.uc_mcontext,regs,set->sig[0]);// 保存siginfoerr|=copy_siginfo_to_user(&frame->info,info);// 设置返回地址restorer=ka->sa.sa_restorer;if(!restorer)restorer=VDSO_SYMBOL(current->mm->context.vdso,rt_sigreturn);err|=__put_user(restorer,&frame->pretcode);// 设置用户栈指针和指令指针regs->sp=(unsignedlong)frame;regs->ip=(unsignedlong)ka->sa.sa_handler;regs->ax=sig;// 信号编号作为第一个参数regs->si=(unsignedlong)&frame->info;// siginfo作为第二个参数regs->dx=(unsignedlong)&frame->uc;// ucontext作为第三个参数returnerr;}1.3 高级信号处理技术
1.3.1 信号掩码与阻塞
// 信号掩码操作完整示例#include<signal.h>#include<stdio.h>#include<unistd.h>#include<string.h>voidprint_signal_mask(constchar*msg){sigset_tcurr_mask;// 获取当前信号掩码if(sigprocmask(SIG_BLOCK,NULL,&curr_mask)==-1){perror("sigprocmask");return;}printf("%s: ",msg);for(inti=1;i<NSIG;i++){if(sigismember(&curr_mask,i)){printf("%d ",i);}}printf("\n");}intmain(){sigset_tnew_mask,old_mask,pending_mask;printf("PID: %d\n",getpid());// 初始化信号集sigemptyset(&new_mask);sigaddset(&new_mask,SIGINT);sigaddset(&new_mask,SIGQUIT);sigaddset(&new_mask,SIGUSR1);// 阻塞SIGINT, SIGQUIT, SIGUSR1if(sigprocmask(SIG_BLOCK,&new_mask,&old_mask)==-1){perror("sigprocmask");return1;}print_signal_mask("Blocked signals");// 睡眠期间,发送的信号将被阻塞printf("Sleeping for 10 seconds...\n");printf("Try sending signals: kill -INT %d\n",getpid());printf(" kill -QUIT %d\n",getpid());printf(" kill -USR1 %d\n",getpid());sleep(10);// 检查未决信号sigpending(&pending_mask);printf("Pending signals: ");for(inti=1;i<NSIG;i++){if(sigismember(&pending_mask,i)){printf("%d ",i);}}printf("\n");// 恢复原来的信号掩码if(sigprocmask(SIG_SETMASK,&old_mask,NULL)==-1){perror("sigprocmask");return1;}print_signal_mask("Restored signal mask");// 现在信号将被递送printf("Signals will be delivered now...\n");sleep(5);return0;}1.3.2 信号处理函数的安全实践
// 异步信号安全的信号处理函数#include<signal.h>#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<errno.h>#include<string.h>// 全局标志,由信号处理函数设置volatilesig_atomic_tflag=0;// 异步信号安全的处理函数voidsignal_handler(intsig){// 只设置标志,不做其他事情flag=1;// 可以安全地使用writeconstcharmsg[]="Signal received\n";write(STDOUT_FILENO,msg,sizeof(msg)-1);}// 非安全的处理函数(演示问题)voidunsafe_handler(intsig){// printf不是异步信号安全的!printf("Received signal %d\n",sig);// 危险!// malloc/free也不是安全的char*buf=malloc(100);// 危险!if(buf){sprintf(buf,"Signal %d",sig);// sprintf也不是安全的free(buf);// 危险!}}// 主程序:安全的信号处理模式intmain(){structsigactionsa;// 设置信号处理sa.sa_handler=signal_handler;sigemptyset(&sa.sa_mask);sa.sa_flags=0;if(sigaction(SIGINT,&sa,NULL)==-1){perror("sigaction");return1;}if(sigaction(SIGTERM,&sa,NULL)==-1){perror("sigaction");return1;}// 主循环:检查标志而不是在信号处理函数中做复杂工作while(1){// 阻塞所有信号以避免竞态条件sigset_tall_signals,old_signals;sigfillset(&all_signals);sigprocmask(SIG_BLOCK,&all_signals,&old_signals);if(flag){// 重置标志flag=0;// 在这里执行实际工作(在信号被阻塞的情况下)printf("Processing signal...\n");// 可以安全地使用非异步安全的函数}// 恢复信号掩码sigprocmask(SIG_SETMASK,&old_signals,NULL);// 休眠(可能被信号中断)pause();// 等待信号}return0;}1.3.3 实时信号与排队信号
// 实时信号(排队信号)示例#include<signal.h>#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<string.h>// 实时信号处理函数voidrt_signal_handler(intsig,siginfo_t*info,void*context){// 可以获取发送者的PID和数据printf("Received real-time signal %d\n",sig);printf(" From PID: %d\n",info->si_pid);printf(" Value: %d\n",info->si_value.sival_int);printf(" Code: %d\n",info->si_code);// 可以使用用户数据if(info->si_code==SI_QUEUE){printf(" This is a queued signal with data\n");}}intmain(){structsigactionsa;unionsigval value;printf("PID: %d\n",getpid());// 设置实时信号处理sa.sa_sigaction=rt_signal_handler;sigemptyset(&sa.sa_mask);sa.sa_flags=SA_SIGINFO|SA_RESTART;// 重要:必须使用SA_SIGINFO// 注册实时信号处理if(sigaction(SIGRTMIN,&sa,NULL)==-1){perror("sigaction");return1;}if(sigaction(SIGRTMIN+1,&sa,NULL)==-1){perror("sigaction");return1;}// 如果是父进程,发送排队信号pid_tpid=fork();if(pid==-1){perror("fork");return1;}if(pid>0){// 父进程:发送多个排队信号sleep(1);// 给子进程时间设置信号处理for(inti=0;i<5;i++){value.sival_int=i*100;printf("Parent: sending signal with value %d\n",value.sival_int);// 发送排队信号(实时信号可以排队)if(sigqueue(pid,SIGRTMIN,value)==-1){perror("sigqueue");}usleep(100000);// 100ms间隔}// 等待子进程wait(NULL);}else{// 子进程:接收信号printf("Child: waiting for signals...\n");// 实时信号会被排队,所以会按顺序处理while(1){pause();// 等待信号}}return0;}二、守护进程(Daemon)深度解析
2.1 守护进程的完整创建流程
2.1.1 传统UNIX守护进程创建
// 完整的守护进程创建函数#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<signal.h>#include<syslog.h>intbecome_daemon(intflags){intmaxfd,fd;// 1. 创建子进程,父进程退出switch(fork()){case-1:return-1;// 失败case0:break;// 子进程继续default:_exit(EXIT_SUCCESS);// 父进程退出}// 2. 成为新会话的首进程,脱离控制终端if(setsid()==-1)return-1;// 3. 确保不是会话首进程(防止获取控制终端)switch(fork()){case-1:return-1;case0:break;default:_exit(EXIT_SUCCESS);}// 4. 清除文件创建掩码if(!(flags&BD_NO_UMASK0))umask(0);// 5. 改变工作目录到根目录if(!(flags&BD_NO_CHDIR))if(chdir("/")==-1)return-1;// 6. 关闭所有打开的文件描述符if(!(flags&BD_NO_CLOSE_FILES)){maxfd=sysconf(_SC_OPEN_MAX);if(maxfd==-1)// 如果限制不确定,假设为1024maxfd=1024;for(fd=0;fd<maxfd;fd++)close(fd);}// 7. 重新打开标准文件描述符到/dev/nullif(!(flags&BD_NO_REOPEN_STD_FDS)){close(STDIN_FILENO);fd=open("/dev/null",O_RDWR);if(fd!=STDIN_FILENO)// fd应该是0return-1;if(dup2(STDIN_FILENO,STDOUT_FILENO)!=STDOUT_FILENO)return-1;if(dup2(STDIN_FILENO,STDERR_FILENO)!=STDERR_FILENO)return-1;}return0;}// 守护进程标志#defineBD_NO_CHDIR01// 不改变工作目录#defineBD_NO_CLOSE_FILES02// 不关闭所有打开的文件#defineBD_NO_REOPEN_STD_FDS04// 不重新打开stdin/stdout/stderr#defineBD_NO_UMASK0010// 不清除umask#defineBD_MAX_CLOSE8192// 如果sysconf返回不确定值2.1.2 现代守护进程的最佳实践
// 使用双重fork和syslog的现代守护进程#include<sys/types.h>#include<sys/stat.h>#include<syslog.h>#include<fcntl.h>#include<signal.h>#include<unistd.h>#include<stdlib.h>#include<stdio.h>#include<string.h>#include<errno.h>// 信号处理函数staticvoidsignal_handler(intsig){switch(sig){caseSIGHUP:syslog(LOG_INFO,"Received SIGHUP, reloading configuration");// 重新加载配置break;caseSIGTERM:syslog(LOG_INFO,"Received SIGTERM, exiting");closelog();exit(EXIT_SUCCESS);default:syslog(LOG_WARNING,"Received unexpected signal %d",sig);}}// 设置信号处理staticvoidsetup_signal_handlers(void){structsigactionsa;// 设置SIGHUP处理(重新加载配置)sa.sa_handler=signal_handler;sigemptyset(&sa.sa_mask);sa.sa_flags=SA_RESTART;if(sigaction(SIGHUP,&sa,NULL)==-1){syslog(LOG_ERR,"Failed to set up SIGHUP handler: %s",strerror(errno));exit(EXIT_FAILURE);}// 设置SIGTERM处理(优雅退出)if(sigaction(SIGTERM,&sa,NULL)==-1){syslog(LOG_ERR,"Failed to set up SIGTERM handler: %s",strerror(errno));exit(EXIT_FAILURE);}// 忽略不必要的信号sa.sa_handler=SIG_IGN;if(sigaction(SIGPIPE,&sa,NULL)==-1){syslog(LOG_ERR,"Failed to ignore SIGPIPE: %s",strerror(errno));exit(EXIT_FAILURE);}if(sigaction(SIGUSR1,&sa,NULL)==-1){syslog(LOG_ERR,"Failed to ignore SIGUSR1: %s",strerror(errno));exit(EXIT_FAILURE);}if(sigaction(SIGUSR2,&sa,NULL)==-1){syslog(LOG_ERR,"Failed to ignore SIGUSR2: %s",strerror(errno));exit(EXIT_FAILURE);}}// 创建PID文件staticintcreate_pidfile(constchar*pidfile){intfd;charpid_str[20];fd=open(pidfile,O_WRONLY|O_CREAT|O_TRUNC,0644);if(fd==-1){syslog(LOG_ERR,"Cannot open PID file %s: %s",pidfile,strerror(errno));return-1;}snprintf(pid_str,sizeof(pid_str),"%ld\n",(long)getpid());if(write(fd,pid_str,strlen(pid_str))!=(ssize_t)strlen(pid_str)){syslog(LOG_ERR,"Cannot write to PID file %s: %s",pidfile,strerror(errno));close(fd);return-1;}close(fd);return0;}// 守护进程主函数intdaemonize(constchar*name,constchar*pidfile,intfacility){pid_tpid,sid;// 第一次forkpid=fork();if(pid<0){// fork失败return-1;}elseif(pid>0){// 父进程退出exit(EXIT_SUCCESS);}// 子进程继续// 创建新会话,脱离控制终端sid=setsid();if(sid<0){return-1;}// 第二次fork,确保不是会话首进程,防止获取控制终端pid=fork();if(pid<0){return-1;}elseif(pid>0){// 父进程退出exit(EXIT_SUCCESS);}// 孙子进程继续(真正的守护进程)// 清除文件创建掩码umask(0);// 改变工作目录到根目录if(chdir("/")<0){return-1;}// 关闭标准文件描述符close(STDIN_FILENO);close(STDOUT_FILENO);close(STDERR_FILENO);// 重新打开stdin, stdout, stderr到/dev/nullopen("/dev/null",O_RDONLY);// stdinopen("/dev/null",O_RDWR);// stdoutopen("/dev/null",O_RDWR);// stderr// 初始化syslogopenlog(name,LOG_PID,facility);// 创建PID文件if(pidfile&&create_pidfile(pidfile)<0){syslog(LOG_ERR,"Cannot create PID file");return-1;}// 设置信号处理setup_signal_handlers();syslog(LOG_INFO,"Daemon %s started with PID %ld",name,(long)getpid());return0;}// 示例守护进程主程序intmain(intargc,char*argv[]){constchar*daemon_name="mydaemon";constchar*pidfile="/var/run/mydaemon.pid";// 成为守护进程if(daemonize(daemon_name,pidfile,LOG_DAEMON)<0){fprintf(stderr,"Failed to daemonize\n");exit(EXIT_FAILURE);}// 守护进程主循环syslog(LOG_INFO,"Daemon is running");while(1){// 守护进程的工作// 例如:检查配置、处理请求等sleep(10);// 示例:每10秒循环一次syslog(LOG_DEBUG,"Daemon heartbeat");}// 理论上不会到达这里closelog();return0;}2.2 systemd时代的守护进程
2.2.1 systemd服务单元文件
# /etc/systemd/system/mydaemon.service [Unit] Description=My Custom Daemon Documentation=man:mydaemon(8) After=network.target Requires=network.target [Service] Type=notify # 使用sd_notify()通知systemd ExecStart=/usr/sbin/mydaemon # 守护进程二进制文件 ExecReload=/bin/kill -HUP $MAINPID ExecStop=/bin/kill -TERM $MAINPID KillSignal=SIGTERM KillMode=process # 只杀死主进程 Restart=on-failure # 失败时重启 RestartSec=5s # 重启前等待5秒 TimeoutStopSec=30s # 停止超时30秒 # 安全设置 User=daemonuser Group=daemongroup NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/var/lib/mydaemon /var/log/mydaemon # 资源限制 LimitNOFILE=65536 LimitNPROC=512 LimitMEMLOCK=infinity # 环境变量 Environment="MY_DAEMON_DEBUG=0" EnvironmentFile=-/etc/default/mydaemon [Install] WantedBy=multi-user.target2.2.2 使用sd-daemon库的现代守护进程
// 使用systemd的sd-daemon库的守护进程#define_GNU_SOURCE#include<systemd/sd-daemon.h>#include<systemd/sd-event.h>#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<signal.h>#include<syslog.h>#include<errno.h>#include<string.h>// 事件回调函数staticintsignal_handler(sd_event_source*es,conststructsignalfd_siginfo*si,void*userdata){switch(si->ssi_signo){caseSIGTERM:sd_notify(0,"STOPPING=1");sd_event_exit(sd_event_source_get_event(es),0);break;caseSIGHUP:sd_notify(0,"RELOADING=1");// 重新加载配置sd_notify(0,"READY=1");break;default:break;}return0;}// 定时器回调函数staticinttimer_handler(sd_event_source*es,uint64_tusec,void*userdata){staticintcount=0;count++;syslog(LOG_INFO,"Timer tick %d",count);// 发送watchdog通知(如果启用了WatchdogSec)sd_notify(0,"WATCHDOG=1");return0;}intmain(intargc,char*argv[]){sd_event*event=NULL;sd_event_source*signal_source=NULL;sd_event_source*timer_source=NULL;sigset_tss;intr;// 初始化syslogopenlog("systemd-daemon",LOG_PID,LOG_DAEMON);// 创建事件循环r=sd_event_default(&event);if(r<0){syslog(LOG_ERR,"Failed to create event loop: %s",strerror(-r));gotofinish;}// 设置信号处理sigemptyset(&ss);sigaddset(&ss,SIGTERM);sigaddset(&ss,SIGHUP);sigaddset(&ss,SIGINT);r=sd_event_add_signal(event,&signal_source,&ss,signal_handler,NULL);if(r<0){syslog(LOG_ERR,"Failed to add signal handler: %s",strerror(-r));gotofinish;}// 添加定时器(每秒触发一次)r=sd_event_add_time(event,&timer_source,CLOCK_MONOTONIC,UINT64_MAX,1000000,timer_handler,NULL);if(r<0){syslog(LOG_ERR,"Failed to add timer: %s",strerror(-r));gotofinish;}// 通知systemd服务已启动sd_notify(0,"READY=1\n""STATUS=Daemon is running\n""MAINPID=%lu",(unsignedlong)getpid());syslog(LOG_INFO,"Daemon started with PID %ld",(long)getpid());// 运行事件循环r=sd_event_loop(event);if(r<0){syslog(LOG_ERR,"Event loop failed: %s",strerror(-r));}finish:// 清理if(signal_source)sd_event_source_unref(signal_source);if(timer_source)sd_event_source_unref(timer_source);if(event)sd_event_unref(event);syslog(LOG_INFO,"Daemon stopped");closelog();returnr<0?EXIT_FAILURE:EXIT_SUCCESS;}三、进程组(Process Group)与会话(Session)
3.1 进程组与会话的基本概念
3.1.1 关系层次
进程、进程组、会话、终端的关系: ┌─────────────────────────────────────────┐ │ 终端(Terminal) │ ← 物理或伪终端 │ /dev/tty1, pts/0, etc. │ └───────────────────┬─────────────────────┘ │ 控制关系 ▼ ┌─────────────────────────────────────────┐ │ 会话(Session) │ ← 一次登录形成一个会话 │ - 会话ID(SID) │ │ - 控制进程(Session Leader) │ │ - 控制终端(Controlling Terminal) │ │ - 前台进程组(Foreground Process Group)│ │ - 后台进程组(Background Process Group)│ └───────────────────┬─────────────────────┘ │ 包含关系 ▼ ┌─────────────────────────────────────────┐ │ 进程组(Process Group) │ ← 一个作业(job) │ - 进程组ID(PGID) │ │ - 组长进程(Process Group Leader) │ └───────────────────┬─────────────────────┘ │ 包含关系 ▼ ┌─────────────────────────────────────────┐ │ 进程(Process) │ │ - 进程ID(PID) │ │ - 父进程ID(PPID) │ │ - 实际用户ID(RUID) │ │ - 有效用户ID(EUID) │ └─────────────────────────────────────────┘3.1.2 内核数据结构
// 内核中的进程组和会话数据结构// task_struct中的相关字段structtask_struct{// ... 其他字段pid_tpid;// 进程IDpid_ttgid;// 线程组ID(POSIX线程)// 进程组和会话structpid_linkpids[PIDTYPE_MAX];// PID链接structtask_struct*group_leader;// 线程组领导者// ... 其他字段};// PID类型枚举enumpid_type{PIDTYPE_PID,// 进程IDPIDTYPE_TGID,// 线程组IDPIDTYPE_PGID,// 进程组IDPIDTYPE_SID,// 会话IDPIDTYPE_MAX};// PID结构structpid{atomic_tcount;unsignedintlevel;structhlist_headtasks[PIDTYPE_MAX];structrcu_headrcu;structupidnumbers[1];};// 用户空间可见的PIDstructupid{intnr;// PID数值structpid_namespace*ns;// 命名空间structhlist_nodepid_chain;};3.2 进程组操作与会话管理
3.2.1 创建新会话和进程组
// 创建新会话和进程组的完整示例#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>#include<signal.h>voidprint_ids(constchar*name){printf("%s: PID=%ld, PPID=%ld, PGID=%ld, SID=%ld\n",name,(long)getpid(),(long)getppid(),(long)getpgrp(),(long)getsid(0));// 获取控制终端inttty=tcgetpgrp(STDIN_FILENO);if(tty==-1){printf("%s: No controlling terminal\n",name);}else{printf("%s: Controlling terminal PGID=%ld\n",name,(long)tty);}}intmain(){pid_tpid;printf("Original process:\n");print_ids("Parent");// 创建子进程pid=fork();if(pid==-1){perror("fork");return1;}if(pid==0){// 子进程// 创建新会话if(setsid()==-1){perror("setsid");exit(1);}printf("\nAfter setsid():\n");print_ids("Child (new session leader)");// 创建孙子进程(在新的会话中)pid_tgrandchild_pid=fork();if(grandchild_pid==-1){perror("fork in child");exit(1);}if(grandchild_pid==0){// 孙子进程// 创建新进程组(孙子进程成为组长)if(setpgid(0,0)==-1){perror("setpgid");exit(1);}printf("\nGrandchild in new process group:\n");print_ids("Grandchild (new PG leader)");// 孙子进程的工作sleep(5);exit(0);}else{// 子进程等待孙子进程wait(NULL);}exit(0);}else{// 父进程sleep(1);printf("\nParent after child created new session:\n");print_ids("Parent");// 父进程等待子进程wait(NULL);}return0;}3.2.2 作业控制(Job Control)实现
// 简单的shell作业控制实现#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>#include<signal.h>#include<termios.h>#include<string.h>#defineMAX_JOBS100// 作业状态typedefenum{JOB_RUNNING,JOB_STOPPED,JOB_DONE}job_state_t;// 作业结构typedefstruct{pid_tpgid;// 进程组IDintjob_id;// 作业IDjob_state_tstate;// 状态char*command;// 命令字符串}job_t;job_tjobs[MAX_JOBS];intnext_job_id=1;// 添加作业intadd_job(pid_tpgid,constchar*command){for(inti=0;i<MAX_JOBS;i++){if(jobs[i].state==JOB_DONE||jobs[i].pgid==0){jobs[i].pgid=pgid;jobs[i].job_id=next_job_id++;jobs[i].state=JOB_RUNNING;jobs[i].command=strdup(command);returnjobs[i].job_id;}}return-1;}// 更新作业状态voidupdate_job_status(pid_tpgid,job_state_tstate){for(inti=0;i<MAX_JOBS;i++){if(jobs[i].pgid==pgid){jobs[i].state=state;break;}}}// 列出所有作业voidlist_jobs(){printf("Job List:\n");printf("ID\tPGID\tState\tCommand\n");for(inti=0;i<MAX_JOBS;i++){if(jobs[i].pgid!=0&&jobs[i].state!=JOB_DONE){constchar*state_str;switch(jobs[i].state){caseJOB_RUNNING:state_str="Running";break;caseJOB_STOPPED:state_str="Stopped";break;default:state_str="Unknown";break;}printf("[%d]\t%ld\t%s\t%s\n",jobs[i].job_id,(long)jobs[i].pgid,state_str,jobs[i].command);}}}// 前台运行命令pid_trun_foreground(constchar*command){pid_tpid=fork();if(pid==0){// 子进程// 创建新进程组,子进程成为组长setpgid(0,0);// 将新进程组设置为前台进程组tcsetpgrp(STDIN_FILENO,getpgrp());// 恢复默认信号处理signal(SIGINT,SIG_DFL);signal(SIGQUIT,SIG_DFL);signal(SIGTSTP,SIG_DFL);signal(SIGTTIN,SIG_DFL);signal(SIGTTOU,SIG_DFL);// 执行命令execl("/bin/sh","sh","-c",command,(char*)NULL);// 如果exec失败perror("execl");exit(1);}elseif(pid>0){// 父进程(shell)// 等待子进程的进程组intstatus;waitpid(-pid,&status,WUNTRACED);// 将控制终端返回给shelltcsetpgrp(STDIN_FILENO,getpgrp());// 检查子进程是否停止if(WIFSTOPPED(status)){// 子进程被停止(如Ctrl+Z)printf("\nJob stopped\n");add_job(pid,command);update_job_status(pid,JOB_STOPPED);}elseif(WIFSIGNALED(status)){// 子进程被信号终止printf("\nJob terminated by signal %d\n",WTERMSIG(status));}returnpid;}return-1;}// 后台运行命令pid_trun_background(constchar*command){pid_tpid=fork();if(pid==0){// 子进程// 创建新进程组setpgid(0,0);// 后台进程应该忽略终端信号signal(SIGINT,SIG_IGN);signal(SIGQUIT,SIG_IGN);signal(SIGTSTP,SIG_IGN);signal(SIGTTIN,SIG_IGN);signal(SIGTTOU,SIG_IGN);// 重定向标准输入从/dev/nullintnull_fd=open("/dev/null",O_RDONLY);if(null_fd!=-1){dup2(null_fd,STDIN_FILENO);close(null_fd);}// 执行命令execl("/bin/sh","sh","-c",command,(char*)NULL);perror("execl");exit(1);}elseif(pid>0){// 父进程(shell)intjob_id=add_job(pid,command);printf("[%d] %ld\n",job_id,(long)pid);returnpid;}return-1;}// 继续运行作业(前台或后台)intcontinue_job(intjob_id,intforeground){job_t*job=NULL;// 查找作业for(inti=0;i<MAX_JOBS;i++){if(jobs[i].job_id==job_id&&jobs[i].state==JOB_STOPPED){job=&jobs[i];break;}}if(!job){printf("No such stopped job: %d\n",job_id);return-1;}// 发送SIGCONT信号继续作业if(kill(-job->pgid,SIGCONT)==-1){perror("kill");return-1;}if(foreground){// 前台继续job->state=JOB_RUNNING;// 将作业的进程组设置为前台进程组tcsetpgrp(STDIN_FILENO,job->pgid);// 等待作业完成intstatus;waitpid(-job->pgid,&status,WUNTRACED);// 将控制终端返回给shelltcsetpgrp(STDIN_FILENO,getpgrp());if(WIFSTOPPED(status)){job->state=JOB_STOPPED;printf("\nJob %d stopped again\n",job_id);}else{job->state=JOB_DONE;printf("\nJob %d completed\n",job_id);}}else{// 后台继续job->state=JOB_RUNNING;printf("Continued job %d in background\n",job_id);}return0;}intmain(){charcommand[256];structtermiosoriginal_termios;// 保存原始终端设置tcgetattr(STDIN_FILENO,&original_termios);printf("Simple Shell with Job Control\n");printf("Commands:\n");printf(" command & - Run in background\n");printf(" jobs - List jobs\n");printf(" fg <job_id> - Bring job to foreground\n");printf(" bg <job_id> - Continue job in background\n");printf(" exit - Exit shell\n");while(1){printf("\nshell> ");fflush(stdout);if(!fgets(command,sizeof(command),stdin)){break;}// 去掉换行符command[strcspn(command,"\n")]='\0';if(strlen(command)==0){continue;}if(strcmp(command,"exit")==0){break;}elseif(strcmp(command,"jobs")==0){list_jobs();}elseif(strncmp(command,"fg ",3)==0){intjob_id=atoi(command+3);continue_job(job_id,1);}elseif(strncmp(command,"bg ",3)==0){intjob_id=atoi(command+3);continue_job(job_id,0);}elseif(command[strlen(command)-1]=='&'){// 后台运行command[strlen(command)-1]='\0';run_background(command);}else{// 前台运行run_foreground(command);}}// 恢复原始终端设置tcsetattr(STDIN_FILENO,TCSANOW,&original_termios);return0;}3.3 终端与控制进程
3.3.1 终端控制原语
// 终端控制完整示例#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<termios.h>#include<signal.h>#include<sys/ioctl.h>// 保存和恢复终端设置structtermiosoriginal_termios;voidrestore_terminal(){tcsetattr(STDIN_FILENO,TCSAFLUSH,&original_termios);}voidsetup_terminal(){structtermiosnew_termios;// 获取当前终端设置if(tcgetattr(STDIN_FILENO,&original_termios)==-1){perror("tcgetattr");exit(1);}// 设置atexit处理函数atexit(restore_terminal);// 配置新终端设置new_termios=original_termios;// 禁用规范模式(逐行处理)和回显new_termios.c_lflag&=~(ICANON|ECHO);// 设置最小读取字符数和超时new_termios.c_cc[VMIN]=1;// 至少读取1个字符new_termios.c_cc[VTIME]=0;// 无超时// 应用新设置if(tcsetattr(STDIN_FILENO,TCSAFLUSH,&new_termios)==-1){perror("tcsetattr");exit(1);}}// 获取终端大小voidget_terminal_size(int*rows,int*cols){structwinsizews;if(ioctl(STDIN_FILENO,TIOCGWINSZ,&ws)==-1){// 如果ioctl失败,使用默认值*rows=24;*cols=80;}else{*rows=ws.ws_row;*cols=ws.ws_col;}}// 终端信号处理voidsigwinch_handler(intsig){introws,cols;// 获取新的终端大小get_terminal_size(&rows,&cols);printf("\rTerminal resized: %dx%d\n",cols,rows);fflush(stdout);}// 简单的终端应用程序intmain(){charch;introws,cols;// 设置信号处理signal(SIGWINCH,sigwinch_handler);// 设置终端setup_terminal();// 获取初始终端大小get_terminal_size(&rows,&cols);printf("Terminal size: %dx%d\n",cols,rows);printf("Press 'q' to quit, any other key to continue\n");while(1){// 读取单个字符(非阻塞模式)if(read(STDIN_FILENO,&ch,1)==1){if(ch=='q'||ch=='Q'){break;}printf("You pressed: %c (0x%02x)\r\n",isprint(ch)?ch:' ',(unsignedchar)ch);// 如果是Ctrl+C,不会中断程序(因为我们处理了终端设置)if(ch==0x03){// Ctrl+Cprintf("Ctrl+C pressed (but ignored)\r\n");}// 如果是Ctrl+Z,发送SIGTSTP给自己if(ch==0x1a){// Ctrl+Zprintf("Ctrl+Z pressed, stopping...\r\n");fflush(stdout);kill(getpid(),SIGTSTP);// 当进程继续时,从这里恢复printf("Process continued\r\n");}}}printf("\nExiting...\n");return0;}四、高级主题与最佳实践
4.1 信号处理的最佳实践
4.1.1 信号处理中的竞态条件
// 避免信号处理中的竞态条件#include<signal.h>#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<errno.h>#include<pthread.h>// 使用sig_atomic_t保证原子性volatilesig_atomic_tflag=0;volatilesig_atomic_tprocessed_flag=1;// 初始为已处理// 信号处理函数 - 只设置标志voidsignal_handler(intsig){if(processed_flag){flag=1;processed_flag=0;}}// 线程安全的信号处理void*worker_thread(void*arg){sigset_tmask;// 在这个线程中阻塞所有信号sigfillset(&mask);pthread_sigmask(SIG_BLOCK,&mask,NULL);while(1){// 在这里执行工作,不会被信号中断usleep(100000);// 100ms// 检查是否需要处理信号(通过线程间通信)// ...}returnNULL;}intmain(){structsigactionsa;pthread_tthread;sigset_tmask,oldmask;// 设置信号处理sa.sa_handler=signal_handler;sigemptyset(&sa.sa_mask);sa.sa_flags=0;if(sigaction(SIGINT,&sa,NULL)==-1){perror("sigaction");return1;}if(sigaction(SIGTERM,&sa,NULL)==-1){perror("sigaction");return1;}// 创建工作者线程pthread_create(&thread,NULL,worker_thread,NULL);// 在主线程中处理信号sigemptyset(&mask);sigaddset(&mask,SIGINT);sigaddset(&mask,SIGTERM);while(1){// 等待信号sigsuspend(&oldmask);// 当信号处理函数返回后,检查标志if(flag&&!processed_flag){// 处理信号printf("Processing signal...\n");// 重置标志flag=0;processed_flag=1;}}pthread_join(thread,NULL);return0;}4.2 现代守护进程架构
4.2.1 基于事件循环的守护进程
// 使用libevent的现代守护进程#include<event2/event.h>#include<event2/listener.h>#include<event2/bufferevent.h>#include<signal.h>#include<syslog.h>#include<unistd.h>#include<stdlib.h>#include<string.h>staticvoidsignal_cb(evutil_socket_tfd,shortevent,void*arg){structevent_base*base=arg;syslog(LOG_INFO,"Received signal, shutting down");event_base_loopbreak(base);}staticvoidaccept_cb(structevconnlistener*listener,evutil_socket_tfd,structsockaddr*addr,intsocklen,void*arg){structevent_base*base=arg;structbufferevent*bev;bev=bufferevent_socket_new(base,fd,BEV_OPT_CLOSE_ON_FREE);if(!bev){syslog(LOG_ERR,"Error creating bufferevent");return;}// 设置回调bufferevent_setcb(bev,NULL,NULL,NULL,NULL);bufferevent_enable(bev,EV_READ|EV_WRITE);}intmain(intargc,char*argv[]){structevent_base*base;structevconnlistener*listener;structevent*signal_event;structsockaddr_insin;// 成为守护进程daemon(1,0);// 打开syslogopenlog("libevent-daemon",LOG_PID,LOG_DAEMON);syslog(LOG_INFO,"Daemon started");// 初始化libeventbase=event_base_new();if(!base){syslog(LOG_ERR,"Could not initialize libevent");return1;}// 设置信号处理signal_event=evsignal_new(base,SIGINT,signal_cb,base);evsignal_add(signal_event,NULL);signal_event=evsignal_new(base,SIGTERM,signal_cb,base);evsignal_add(signal_event,NULL);// 设置监听socketmemset(&sin,0,sizeof(sin));sin.sin_family=AF_INET;sin.sin_port=htons(8888);listener=evconnlistener_new_bind(base,accept_cb,base,LEV_OPT_REUSEABLE|LEV_OPT_CLOSE_ON_FREE,-1,(structsockaddr*)&sin,sizeof(sin));if(!listener){syslog(LOG_ERR,"Could not create listener");return1;}syslog(LOG_INFO,"Daemon listening on port 8888");// 运行事件循环event_base_dispatch(base);// 清理evconnlistener_free(listener);event_base_free(base);syslog(LOG_INFO,"Daemon stopped");closelog();return0;}五、总结
信号处理、守护进程、进程组和会话是Linux进程管理中的核心概念。理解这些概念对于编写健壮的、功能丰富的应用程序至关重要。在实际开发中,我们经常需要处理信号、编写守护进程,并通过进程组和会话来管理作业。