Ⅰ. 终端

用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal),fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。

默认情况下(没有重定向),每个进程的标准输入标准输出标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。

Linux系统编程笔记(11)——终端、作业、守护进程-萤火

以输入队列为例,从键盘输入的字符经线路规程(line discipline)过滤后进入输入队列,用户程序以先进先出的顺序从队列中读取字符,一般情况下,当输入队列满的时候再输入字符会丢失,同时系统会响铃警报。终端可以配置成回显(Echo)模式,在这种模式下,输入队列中的每个字符既送给用户程序也送给输出队列,因此我们在命令行键入字符时该字符不仅可以被程序读取,我们也可以同时在屏幕上看到该字符的回显

Linux系统编程笔记(11)——终端、作业、守护进程-萤火

网络终端或图形终端窗口的数目却是不受限制的,这是通过伪终端(Pseudo TTY)实现的。一套伪终端由一个主设备(PTY Master)和一个从设备(PTY Slave)组成。主设备在概念上相当于键盘和显示器,只不过它不是真正的硬件而是一个内核模块,操作它的也不是用户而是另外一个进程。从设备的底层驱动程序不是访问硬件而是访问主设备。

Linux系统编程笔记(11)——终端、作业、守护进程-萤火

查看终端对应的设备文件名

#include <unistd.h>

char *ttyname(int fd);
#include <stdio.h>
#include <unistd.h>

/**
 * @brief 查看标准输入、标准输出、标准错误输出的设备文件名
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    printf("fd %d:%s\n", 0, ttyname(0));  //标准输入
    printf("fd %d:%s\n", 1, ttyname(1));  //标准输出
    printf("fd %d:%s\n", 2, ttyname(2));  //标准错误输出

    return 0;
}

Ⅱ. 作业

Shell分前后台来控制的不是进程而是作业(Job)或者进程组(Process Group)。一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell可以同时运行一个前台作业和任意多个后台作业,这称为作业控制(Job Control)

Linux系统编程笔记(11)——终端、作业、守护进程-萤火
控制终端相同的进程组,属于同一个会话(Session)

比较命令:

%ps -o pid,ppid,pgrp,session,tpgid,comm | cat
  PID  PPID  PGRP  SESS TPGID COMMAND
 7690  7689  7690  7690 14886 zsh
14886  7690 14886  7690 14886 ps
14887  7690 14886  7690 14886 cat
  • ps和cat是zsh的子进程,因此它们的ppid就是zsh的pid
  • ps和cat同属于一个process group,因此它们的组pgrp一样且为ps的pid(因为ps先执行)
  • bash自己就是一个process group,因此pgrp 就是它自己的pid
  • 这三个进程属于同一个session,Leader是zsh,因此是它的pid
  • 当前前台在运行的是ps和cat,它们又是一个组的,因此前台组id(tpgid)就是它们的pgrp
%ps -o pid,ppid,pgrp,session,tpgid,comm | cat &
  PID  PPID  PGRP  SESS TPGID COMMAND
 7690  7689  7690  7690  7690 zsh
15468  7690 15468  7690  7690 ps
15469  7690 15468  7690  7690 cat
  • &就是放到后台运行
  • 当前前台在运行的是bash,因此前台组id(tpgid)就是bash的pgrp

Ⅲ. 守护进程

Linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,不能直接和用户交互。其它进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程不受用户登录注销的影响,它们一直在运行着。这种进程有一个名称叫守护进程(Daemon)

创建守护进程最关键的一步是调用setid函数创建一个新的Session,并成为 Session Leader

#include <sys/types.h>
#include <unistd.h>

pid_t setsid(void);

该函数调用成功时返回新创建的Session的id(其实也就是当前进程的id),出错返回-1。注意,调用这个函数之前,当前进程不允许是进程组(process group)的Leader,否则该函数返回-1。

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char const *argv[]) {
    pid_t pid = fork();  //子进程肯定不是leader
    if (pid < 0) {
        perror("fork");
        exit(-1);
    }
    if (pid) {
        //父进程不需要,可以结束了
        exit(0);
    }

    pid_t sid = setsid();
    printf("new session id is %d\n", sid);

    //修改守护进程的工作目录
    if (chdir("/") < 0) {
        perror("chdir");
        exit(-2);
    }
    //关闭打开着的文件描述符(原session的)
    close(0);
    open("dev/null", O_RDWR);  //返回值是0,因为刚把文件描述符0关闭了,而open会返回最小的可用的文件描述符
    dup2(0, 1);                //将标准输出重定向到dev/null
    dup2(0, 2);

    //让守护进程一直运行着
    while (1) {
        sleep(1);
    }

    return 0;
}