Linux多进程

159 阅读7分钟

基础知识

  • Linux内核为每个进程分配了PCB(task_struct),并加入对应的列表,对进程的管理,其实就是对PCB、PCB链表的操作
  • 增加:
    • 分配PCB内存,并填入相应信息
    • 复制父进程资源(写时才复制)
    • 加入多个链表,如全局链表、调度队列等
  • 删除:
    • 发送终止信号(如 SIGTERM
    • 释放资源
    • 从所有链表中移除对应的PCB
    • 父进程通过wait()回收僵尸进程
  • 修改:更新PCB内容
  • 查询:从链表查找对应指定进程的PCB
  • PCB记录着程序的信息,如:
    • 进程ID:唯一标识一个进程。
    • 状态:记录当前进程的运行状态(如就绪、运行、等待等)。
    • 优先级:用于进程调度,优先级高的进程更容易获得CPU时间。
    • 内存指针:指向进程的代码段、数据段、栈段。
    • 程序计数器:记录下一条需要执行的指令的地址。
    • I/O状态信息:列出与该进程相关的I/O设备和文件。
  • PCB会处于多个链表中,且每个链表都是双向链表;在PCB中嵌入多个链表(如下代码),每次操作PCB都需要操作多个链表
    struct list_head { 
        struct list_head *next, *prev; 
    };
    struct task_struct { 
        // 进程的其他字段 
        struct list_head tasks; // 所有进程的链表 
        struct list_head children;// 子进程链表 
        struct list_head sibling; // 兄弟进程链表 
        struct list_head ptrace_list; // ptrace 跟踪链表 
        // 更多链表节点... };
    
  • 万物皆文件:所有进程都被管理在虚拟文件系统/proc/(本身不占据磁盘空间)之下,如pid=1234的进程就对应/proc/1234,里面记录着进程所有信息,如文件打开表记录在/proc/1234/fd/之下,里面存储着链接文件,命名就是文件描述符(句柄),如下所示: image.png

    ps指令就是去读取/proc/中的文件,并提取需要输出的信息,按照格式进行输出

  • 僵尸进程(defunct):进程已经终止,但是父进程没有调用wait()/waitpid()回收资源,PCB仍保留在链表中
  • 孤儿进程:父进程先总是,子进程此时仍在运行,且由init(1号)进程接管,init进程一直都在运行wait(),等待回收孤儿进程

    僵尸进程-->孤儿进程:当主进程退出都没有回收僵尸进程的资源,此时僵尸进程会被init接管变为孤儿进程,进而被回收;所以ps看见的僵尸进程(状态为z)都是父进程没有结束而导致的。

  • 进程组:多个进程的集合,每个进程都有唯一的PGID,通常为组长进程的PID,子进程会继承父进程的PGID,只有组内所有进程都消失才算该组结束。

    cmd1 | cmd2其实就是一个进程组

  • 会话:多个进程组的集合;当有新的用户登录Linux时,登录进程会为这个用户创建一个会话。用户的登录shell就是会话的首进程。会话的首进程ID会作为整个会话的ID。会话是一个或多个进程组的集合,囊括了登录用户的所有活动。

    会话和进程组都是为了方便管理终端,当然还有创建守护进程也与之密切关系

基本函数

  • pid_t getpid(void);

  • pid_t getppid(void);

    • 获取父进程号
    • 在终端中使用./启动程序时,此时终端bash就是该进程的父进程
  • int setpgid(pid_t pid, pid_t pgid);

    • 修改自身或子进程的PGID
    • 子进程执行exec()后就不能修改其PGID
  • pid_t getpgid(pid_t pid);

    • 获取组号,默认为父进程pid
  • pid_t setsid(void);

    • (不能在组长进程中调用)只能在子进程中调用
    • 子进程完全独立出来,与终端不再是一个会话,从而脱离终端控制
    • 调用后,子进程变成新会话首进程以及新进程组组长
  • pid_t fork(void);

    • 变量(堆)读时共享,写时拷贝
    • 如果共享代码中分配了堆,那子进程、父进程中都需要释放
    • valgrind ./可执行文件,来查看内存泄漏
  • void exit(int status);

    • 参数是返回给父进程的参数,低8位有效
    • 终结该进程,并回收资源(除了pcb),如堆栈代码段、文件描述符等
    • 会刷新io缓冲区,将内存中的内容写入文件中
  • void _exit(int status);

    • 功能一样,但是是直接杀死进程,并清理资源
    • io缓冲区的内容直接清理
  • pid_t wait(int *status);

    • 父进程阻塞等待子进程退出,若没有子进程则立即返回,用于回收子进程的pcb并获取状态
    • 传入的参数用于接受子进程调用exit传出的参数,status需要搭配成套宏函数使用,来判断返回的状态
    • 返回退出的子进程pid
  • pid_t waitpid(pid_t pid,int *status,int options);

    • 和wait差不多,只是wait是阻塞等待,waitpid可配置
    • 参数说明
      • pid>0 等待指定进程
      • pid=0 等待同一组中任何进程
      • pid=-1 等待任一子进程
      • pid<-1 等待指定进程组中任何子进程,进程组为pid绝对值
      • status 和wait中一样
      • options=0 阻塞等待
      • options=WNOHANG:没有任何已经结束的子进程,则立即返回
    • 返回说明:
      • 0 子进程正常退出

      • =0 非阻塞模式,子进程正在运行
      • <0 子进程异常退出或已经没有子进程
    •  int status; 
       pid_t pid; 
       while ((pid = waitpid(-1, &status, WNOHANG)) == 0) 
       { //轮询等待回收资源
           printf("无子进程退出,父进程继续执行其他任务\n"); 
           sleep(1); 
       }
      

    status

    • 父进程无法直接读取子进程中的变量,子进程也无法直接写入父进程变量中,子进程只能写入os分配的空间,然后父进程调用wait、waitpid将数据移动到自己的变量中
    • status是按位写入的,整体的指并没有作用
      • 低7位记录信号,比如kill -9 pid,就会记录信号为9
      • 低第8位没有使用
      • 次低8位:进程退出时所对应的退出状态(退出码)
    printf("waitpid返回的退出码:%d,信号:%d", status >> 8 & 0xFF, status & 0x7F);
    
  • 进程替换函数exec族函数

    参考链接
    调用成功后,就使用指定的程序来替换当前进程,新老进程,就代码、数据不同,且会从新程序的main从头执行,老程序的代码全部抛弃,包括没有执行的部分 调用失败,则出错返回-1

    exec函数族分别是:execl, execlp, execle, execv, execvp, execvpe,规律如下:
    - l(list)  : 表示参数采用列表,NULL作为参数结束标志;
    - v(vector)  : 参数用数组;
    - p(path)  : 有p自动搜索环境变量PATH,所以第一个参数只用指定名字就行,而不需要路径; - e(env)  : 表示自己维护环境变量;

    execl("/usr/bin/ls","ls","-a","-l",NULL);
    execlp("ls","ls","-a","-l",NULL);
    //第一个ls代表路径,需要用此去环境变量中查找路径
    //第二个ls代表在命令行中如何调用此指令(程序)
    char* const argv[] = {
      "ls",
      "-a",
      "-l",
      NULL 
    };
    execv("/usr/bin/ls",argv);
    execvp("ls",argv);
    

创建守护进程

ps ajx

步骤

  1. 创建子进程,父进程退出
  2. 子进程中创建新会话
  • setsid()
  • 子进程脱离控制
  1. 改变当前目录位置(一般改为根目录下)
  • chdir()
  • 因为守护进程是一直运行的,所以不能占用可卸载的文件系统(如U盘)
  1. 重设文件权限掩码
  • umask(八进制)
  • 默认的文件为666,目录为 777
  • 实际权限 = 默认权限 & (~umask)
  • 如umask(0); // 权限掩码设为0,文件默认权限为666 & ~0 = 666,目录为777 & ~0 = 777
  1. 关闭文件描述符(0、1、2一般是重定向)
  • 继承的打开文件不会用到,浪费系统资源
  • 且由于脱离了终端,那标准输入输出就无法使用
  • close()open("/dev/null",O_RDWR)dup2(newfd,oldfd);
    for (int i = 0; i < sysconf(_SC_OPEN_MAX); i++) { 
        close(i); 
    }
    int fd = open("/dev/null", O_RDWR); 
    dup2(fd, STDIN_FILENO); // 标准输入 
    dup2(fd, STDOUT_FILENO); // 标准输出 
    dup2(fd, STDERR_FILENO); // 标准错误
    
  1. 执行核心工作