再谈Linux多进程——进程处理与守护进程
进程是操作系统资源分配的基本单位,进程是程序执行的实例!!!
Linux 内核中管理进程关键数据结构(进程控制块:PCB)
struct task_struct
{volatile long state; //说明了该进程是否可以执行,还是可中断等信息unsigned long flags; // flags 是进程号,在调用fork()时给出int sigpending; // 进程上是否有待处理的信号mm_segment_t addr_limit; //进程地址空间,区分内核进程与普通进程在内存存放的位置不同 //0-0xBFFFFFFF for user-thead //0-0xFFFFFFFF for kernel-thread//调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度volatile long need_resched;int lock_depth; //锁深度long nice; //进程的基本时间片//进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHERunsigned long policy;struct mm_struct *mm; //进程内存管理信息 (内存管理结构)int processor;//若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新unsigned long cpus_runnable, cpus_allowed;struct list_head run_list; //指向运行队列的指针unsigned long sleep_time; //进程的睡眠时间//用于将系统中所有的进程连成一个双向循环链表, 其根是init_taskstruct task_struct *next_task, *prev_task;struct mm_struct *active_mm;struct list_head local_pages; //指向本地页面 unsigned int allocation_order, nr_local_pages;struct linux_binfmt *binfmt; //进程所运行的可执行文件的格式int exit_code, exit_signal;int pdeath_signal; //父进程终止时向子进程发送的信号unsigned long personality;//Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序int did_exec:1; pid_t pid; //进程标识符,用来代表一个进程pid_t pgrp; //进程组标识,表示进程所属的进程组pid_t tty_old_pgrp; //进程控制终端所在的组标识pid_t session; //进程的会话标识pid_t tgid;int leader; //表示进程是否为会话主管struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr;struct list_head thread_group; //线程链表struct task_struct *pidhash_next; //用于将进程链入HASH表struct task_struct **pidhash_pprev;wait_queue_head_t wait_chldexit; //供wait4()使用struct completion *vfork_done; //供vfork() 使用unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值//it_real_value,it_real_incr用于REAL定时器,单位为jiffies, 系统根据it_real_value//设置定时器的第一个终止时间. 在定时器到期时,向进程发送SIGALRM信号,同时根据//it_real_incr重置终止时间,it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。//当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送//信号SIGPROF,并根据it_prof_incr重置时间.//it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种//状态下,每个tick都使it_virt_value值减一当减到0时,向进程发送信号SIGVTALRM,根据//it_virt_incr重置初值。unsigned long it_real_value, it_prof_value, it_virt_value;unsigned long it_real_incr, it_prof_incr, it_virt_value;struct timer_list real_timer; //指向实时定时器的指针struct tms times; //记录进程消耗的时间unsigned long start_time; //进程创建的时间//记录进程在每个CPU上所消耗的用户态时间和核心态时间long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS]; //内存缺页和交换信息://min_flt, maj_flt累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换//设备读入的页面数); nswap记录进程累计换出的页面数,即写到交换设备上的页面数。//cmin_flt, cmaj_flt, cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。//在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;int swappable:1; //表示进程的虚拟地址空间是否允许换出//进程认证信息//uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid//euid,egid为有效uid,gid//fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件//系统的访问权限时使用他们。//suid,sgid为备份uid,giduid_t uid,euid,suid,fsuid;gid_t gid,egid,sgid,fsgid;int ngroups; //记录进程在多少个用户组中gid_t groups[NGROUPS]; //记录进程所在的组//进程的权能,分别是有效位集合,继承位集合,允许位集合kernel_cap_t cap_effective, cap_inheritable, cap_permitted;int keep_capabilities:1;struct user_struct *user;struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息unsigned short used_math; //是否使用FPUchar comm[16]; //进程正在运行的可执行文件名int link_count, total_link_ count; //文件系统信息//NULL if no tty 进程所在的控制终端,如果不需要控制终端,则该指针为空struct tty_struct *tty;unsigned int locks;//进程间通信信息struct sem_undo *semundo; //进程在信号灯上的所有undo操作struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作//进程的CPU状态,切换时,要保存到停止进程的task_struct中struct thread_struct thread;struct fs_struct *fs; //文件系统信息struct files_struct *files; //打开文件信息 (打开文件表)spinlock_t sigmask_lock; //信号处理函数struct signal_struct *sig; //信号处理函数sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位struct sigpending pending; //进程上是否有待处理的信号unsigned long sas_ss_sp;size_t sas_ss_size;int (*notifier)(void *priv);void *notifier_data;sigset_t *notifier_mask;u32 parent_exec_id;u32 self_exec_id;spinlock_t alloc_lock;void *journal_info;
}
通过命令行直接运行程序或者使用系统的启动脚本自动启动程序,表象上的一种创建方式,其底层本质上还是通过fork、exec去创建进程。
# 1. 命令行直接运行
./program
# 或者指定完整路径
/usr/bin/program
# 2. 后台运行
nohup ./program 2<&1 > /dev/null &
# 3. 通过脚本启动
bash start_script.sh
fork()函数
完整复制父进程的地址空间(采用写时复制COW技术),子进程继承父进程的文件描述符、信号处理等资源,子进程获得新的PID和独立的内存空间。使用场景: • 需要与父进程共享环境的子进程 • 并行任务处理
(COW:copy-on-write)是一种内存管理技术,将复制操作推迟到第一次写入时进行:在创建一个新副本时,不会立即复制资源,而是共享原始副本的资源;当修改时再执行复制操作。该技术最初产生于Unix系统,用于实现一种傻瓜式的进程创建,最早的fork会将调用进程的所有内容原封不动的拷贝到新产生的子进程中去,这些拷贝的动作很消耗时间,而如果fork完之后我们马上就调用exec,这些辛辛苦苦拷贝来的东西又会被立刻抹掉,所以为了效率,现在的fork结束后并不立刻复制父进程的内容,而是到了真正实用的时候才复制,这样如果下一条语句是exec,它就不会白白作无用功了,也就提高了效率。这是实现原理。掌握这种理念,可以应用在自己的程序设计开发中。
#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "sys/wait.h"/*fork() 通过复制调用进程来创建一个新进程。新进程称为子进程。调用进程称为父进程。 RETURN VALUE 成功返回pid号,其中-1表示失败,0表示是子进程。*/int main() {pid_t pid = fork();if (pid < 0) {perror("fork");exit(1);}if (pid == 0) {printf("child process: pid = %d, ppid = %d\n", getpid(), getppid());sleep(5);} else {printf("parent process: pid = %d, ppid = %d\n", getpid(), getppid());//等待子进程结束,也可以用wait,waitpid可以指定等待的子进程,而wait是等待任意一个子进程结束waitpid(pid, NULL, 0);//wait(NULL);}return 0;
}
exec函数族
完全替换当前进程的代码段,并保持进程ID不变,有多个变体处理不同参数传递方式。使用场景: • 执行外部程序 • 改变当前进程映像 • 配合fork()实现"fork-exec"模式
/*path:要执行的程序的路径。arg:程序的参数,第一个参数通常是程序名,后面是程序需要的参数列表,最后必须以(char *)NULL结束*/
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
execl("/path/to/prog", "prog", "arg1", NULL); // 参数列表/*path:要执行的程序的路径。argv:是一个字符串数组,表示程序的参数列表,第一个元素通常是程序名,最后一个元素必须是NULL*/
int execv(const char *path, char *const argv[]);
execv("/path/to/prog", argv); // 参数数组char *argv[] = { "ls", "-l", NULL };
execv("/bin/ls", argv);/*path:要执行的程序的路径。参数列表以(char *)NULL结束,然后传入环境变量数组envp*/
int execle(const char *path, const char *arg, ... /*, (char *) NULL, char *const envp[] */);
char *envp[] = { "HOME=/usr/home", "LOGNAME=home", NULL };
execle("/bin/ls", "ls", "-l", NULL, envp);execle("/path/to/prog", "prog", NULL, envp); // 自定义环境
用例 (注意:exec成功后不会返回!!!)
#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "sys/wait.h"int main() {pid_t pid = fork();if (pid < 0) {// 错误处理perror("fork failed");exit(1);} else if (pid == 0) {// 子进程execl("/bin/ls", "ls", "-l", NULL);// 如果exec执行成功,不会执行到这里// 如果执行到这里,说明exec失败perror("exec failed");exit(1);} else {// 父进程wait(NULL); // 等待子进程结束}return 0;
}
vfork()函数
轻量级进程创建方式,其子进程共享父进程地址空间,子进程优先运行,父进程阻塞,子进程必须立即调用exec()或_exit();在内存受限系统或需要立即执行新程序的场景中使用。
#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "sys/wait.h"int main() {pid_t pid = vfork();if (pid == 0) {execl("/bin/ls", "ls", "-l", NULL);_exit(127); // 仅exec失败时执行} else if (pid > 0) {int status;waitpid(pid, &status, 0);}return 0;
}
一般场景: 使用 fork() - 最通用、最安全的选择
执行新程序: 使用 fork() + exec() 组合
避免的做法: 不要在 vfork() 子进程中执行复杂操作
特殊需求: 需要细粒度控制资源共享时使用 clone()
进程分离
进程分离是指一个进程能够脱离其父进程,即使父进程终止后仍能继续运行。当子进程与其父进程分离时,它会成为一个后台进程或守护进程,能够独立继续运行而不受父进程生命周期的影响。
#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "sys/wait.h"int main() {pid_t pid = fork();if (pid < 0) {perror("fork");exit(1);}if (pid == 0) {setsid();//创建一个新的会话和进程组,子进程分离sleep(10);printf("child process: pid = %d, ppid = %d\n", getpid(), getppid());} else {printf("parent process: pid = %d, ppid = %d\n", getpid(), getppid());}return 0;
}
守护进程
守护进程是在后台运行的一种特殊进程,与终端或用户会话无关。它们通常在系统启动时启动,并持续运行直到系统关闭。规范的守护进程创建过程中需要进行两次 fork,这是防止守护进程受到终端相关的信号影响,这种方式虽然看起来有点复杂,但是能够最大程度地确保守护进程的独立性和稳定性。
#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "sys/stat.h"
#include "sys/wait.h"
#include "fcntl.h"int main() {// 第一次 forkpid_t pid = fork();if (pid < 0) {exit(1);}if (pid > 0) {// 父进程退出exit(0);}// 创建新会话if (setsid() < 0) {exit(1);}// 第二次 forkpid = fork();if (pid < 0) {exit(1);}if (pid > 0) {// 第一子进程退出exit(0);}//第二次 fork 后不需要调用 setsidchdir("/");umask(0);// 关闭0、1、2文件描述符close(0);close(1);close(2);// 将标准输入、输出和错误重定向到 /dev/nullopen("/dev/null", O_RDWR);dup(0);dup(0);printf("child process: pid = %d, ppid = %d\n", getpid(), getppid());// 在此处添加你的守护进程任务代码return 0;
}
僵尸进程
子进程死亡时父进程没有回收,这样就造成子进程资源无法回收,通常用 ps 可以看到它被显示为defunct,这样就产生了僵尸进程。它将永远保持这样直到父进程回收。
避免僵尸进程手段
父进程主动回收子进程(wait/waitpid);
父进程注册SIGCHLD信号,在回调中进程回收;
父进程忽略SIGCHLD、SIGCLD信号,子进程结束后由内核回收;
fork两次,父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。
// 方法1: 父进程调用wait/waitpid
while (waitpid(-1, NULL, WNOHANG) > 0); //方法2: 父进程注册SIGCHLD信号,在回调中进程回收;
sigaction(SIGCHLD, &sigaction_fd, NULL); // 方法3: 忽略SIGCHLD信号
signal(SIGCHLD, SIG_IGN);// 方法4: 双fork技术
if (fork() == 0) { if (fork() == 0) { // 实际工作进程 } exit(0); // 中间进程立即退出
}
wait(NULL); // 父进程回收中间进程
进程间通信选择
场景 | 推荐IPC | 原因 |
大数据传输 | 共享内存 | 零拷贝,速度最快 |
结构化消息 | 消息队列 | 自带消息类型和优先级 |
简单数据流 | 管道/命名管道 | 使用简单 |
跨主机通信 | Socket(套接字) | 网络透明 |
异步事件通知 | 信号 | 轻量级 |
进程控制是庞大的知识脉络,是系统编程和进程管理的重点,未来也将持续逐部分浅谈与分享!!!