Linux 终端、进程组、会话、守护进程

一、终端概念

终端概念

1、终端(Terminal)

终端是物理设备,只用于输入输出,本身没有强大的计算能力。在计算资源紧张的时代,人们想共享一台计算机,可以通过终端连接到计算机上,将指令输入终端,终端传送给计算机,计算机完成指令后,将输出传送给终端,终端将结果显示给用户,所以有显示器和键盘能够通过串口连接到计算机的设备就叫终端

2、控制台(Console)

控制台也是物理设备,也用于输入输出,但它直接连接在计算机上,是计算机系统的一部分,所以直接连接在电脑上的键盘和显示器就叫做控制台。计算机启动的时候,所有的信息都会显示到控制台上,而不会显示到终端上。简单的说,能直接显示系统消息的那个终端称为控制台,其他的则称为终端

3、控制台与终端的区别

终端是通过串口连接上的,不是计算机本身就有的设备,而控制台是计算机本身就有的设备,一个计算机只有一个控制台。也就是说,控制台是计算机的基本设备,而终端是附加设备。

简言之,终端为主机提供了人机接口,每个人都通过终端使用主机的资源。终端有字符终端和图形终端两种。一台主机可以连很多终端。
控制台是一种特殊的人机接口(控制台也是终端的一种), 是人控制主机的第一人机接口,而主机对于控制台的信任度高于其他终端

4、TTY

TTY 是 Teletype 或 Teletypewriter 的缩写,原来是指电传打字机,后来这种设备逐渐键盘和显示器取代。不管是电传打字机还是键盘显示器,都是作为计算机的终端设备存在的,所以 TTY 也泛指计算机的终端设备

5、虚拟控制台(Virtual Console)与虚拟终端(Virtual Terminal)

虚拟控制台和虚拟终端是一样的:如果我们只有一台终端,这是我们与计算机之间的用户接口,假如有一天,我们想拥有多个用户接口,那么,一方面我们可以增加终端数目,另一方面,还可以在同一台终端上虚拟出多个终端,它们之间互相不影响,至少看起来互相不影响。这些终端就是虚拟终端。Linux 默认所有虚拟终端都是控制台,都能显示系统消息,所以我们在平时的使用中压根就不区分 Linux 中的终端与控制台。

  • 在 Ubuntu 桌面版中,一共有七个虚拟终端:tty1-7(服务器版一般有六个虚拟终端:tty1-6),其中第七个虚拟终端是图形用户界面。
  • 我们可以使用 CTRL+ALT+fn 切换虚拟终端,例如: 我们按下 Ctrl+Alt+F1 时,会进入 tty1。

6、模拟终端器

如今,终端不再是一个需要通过 UART 连接到计算机上物理设备。终端成为内核的一个模块,它可以直接向 TTY 驱动发送字符,并从 TTY 驱动读取响应然后打印到屏幕上。也就是说,用内核模块模拟物理终端设备,因此被称为终端模拟器,所以终端是一种物理设备,而终端模拟器,是一个程序,这些程序用来模拟物理终端

7、shell

和之前说的几个概念截然不同,之前的几个概念都是与计算机的输入输出相关的,而shell是和内核相关的。内核为上层的应用提供了很多服务,shell在内核的上层,在应用程序的下层。例如,你写了一个 hello world 程序,你并不用显式地创建一个进程来运行你的程序,你把写好的程序交给 shell 就行了,由 shell 负责为你的程序创建进程。

我们在终端模拟器中输入命令时,终端模拟器本身并不解释执行这些命令,它只负责输入输出,真正解释执行这些命令的,是 shell。我们平时使用的 sh, bash, csh 是shell 的不实现。

控制终端

由于在 linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端(Controlling Terminal),当控制终端被关闭时,相应的进程都会自动关闭:用户通过终端登录系统后得到一个 Shell 进程,这个终端成为 Shell 进程的控制终端,进程中,控制终端是保存在 PCB 的信息中,而 fork 会复制 PCB 中的信息,因此由 Shell 进程启动的其它进程的控制终端也是这个终端。

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

[[10_ Linux 进程通讯#四、进程间通讯之信号|信号]]一节中还讲过,在控制终端输入一些特殊的控制键可以给前台进程发信号,例如 Ctrl+C 表示 SIGINT,Ctrl+\ 表示 SIGQUIT。

Linux 中所讲的终端,除非特别说明,否则一般都是指控制终端。
(1)tty 命令可以查看当前终端的名字。

yxm@192:~$ tty
/dev/pts/1

(2)也可以通过函数查看终端的名字,函数说明如下:

#include <unistd.h>
char *ttyname(int fd);
功能:由文件描述符查出对应的文件名
参数:
    fd:文件描述符
返回值:
    成功:终端名
    失败:NULL

下面我们借助 ttyname 函数,通过实验看一下各种不同的终端所对应的设备文件名:

#include <stdio.h>
#include <unistd.h>
int main() {
    printf("fd 0: %s\n", ttyname(0));
    printf("fd 1: %s\n", ttyname(1));
    printf("fd 2: %s\n", ttyname(2));
    return 0;
}
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test 
fd 0: /dev/pts/0
fd 1: /dev/pts/0
fd 2: /dev/pts/0

(3)ps -aux 中 TTY 字段:

  1. TTY 字段显示的是 tty1-tty6 说明是在本地机器的命令行下登录的。 tty7 说明是在本地机器的图形界面下登录的
  2. TTY 字段显示的是 pts 说明是用远程工具连接的,比如 xshell,后面的数字代表登录的时间顺序,越小证明登录的越早。

(4)一般情况下,当前终端也是一个进程,在当前终端开启的程序,它的父进程一般也是当前终端

二、进程组概念

进程组概述

进程组,也称之为作业。BSD 于 1980 年前后向 Unix 中增加的一个新特性。代表一个或多个进程的集合。所以进行组由一个或多个共享同一进程组的进程组成。操作系统设计的进程组的概念,是为了简化对多个进程的管理

每个进程都属于一个进程组。当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组 ID 为第一个进程的 ID,进程组的第一个进程称为组长进程(也叫首进程)。所以,对于组长进程标识而言,其进程组 ID 为其本身进程 ID。进程组的标识符为 PGID:

进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程离开(终止或转移到另一个进程组)组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员:只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。

进程组的概念在 [[09_Linux 进程基础#4、进程回收|waitpid 函数]] 和 [[09_Linux 进程基础#2、进程控制命令|kill ]]中都曾使用到。例如可以使用 kill -SIGKILL -进程组ID (负的)来将整个进程组内的进程全部杀死。

前台后台进程

Linux是一种用户控制的多作业操作系统, 系统允许多个系统用户同时提交作业,而一个系统用户可以用多个 shell 登录,每个系统用户可以用一个 shell 提交多个作业。作业有两种运行方式:前台运行和后台运行。

  • 前台运行:能够控制当前终端或窗口,并且接受用户输入;
  • 后台运行:不在当前激活的终端或窗口中运行,而是在用户“看不到”的情况下运行。
    以前台运行方式运行的进程称作前台进程;以后台运行方式运行的进程称作后台进程。

前台进程与后台进程特点如下:

  • 前台进程:
    • 在终端中启动运行的程序,那么该终端就为进程的控制终端,一旦这个终端关闭,这个进程也随之消失。
    • 只有前台进程才能从控制终端中读取与输入。
    • 一般情况下,前台进程的父进程是当前终端。
    • 使用 CTRL+C 可以强行终止前台进程。
  • 后台进程:
    • 后台进程与控制终端脱离(【注意】后台程序并未完全脱离终端,关闭当前终端也导致该后台进程退出,此处与守护进程存在区别,详细可以看下文“守护进程”一节内容)。
    • 后台进程无法控制读取与输入:后台进程并未完全脱离终端,所以在终端未关闭前还是会往终端输出结果,只是输出并不受控制终端控制,具体表现是程序王终端输出结果,同时终端还会出现命令提示符,可以在终端输入命令。
    • 后台进程的父进程为系统进程(1号进程)
    • 使用 CTRL+C 无法终止后台进程,需要使用 kill -9 NNN 才能强行终止后台进程

进程组相关 API

一个进程可以为自己或子进程设置进程组 ID。

#include <unistd.h>

pid_t getpgrp(void);                 /* POSIX.1 version */
功能:获取当前进程的进程组ID
参数:无
返回值:总是返回调用者的进程组ID

pid_t getpgid(pid_t pid);
功能:获取指定进程的进程组ID
参数:
    pid:进程号,如果pid = 0,那么该函数作用和getpgrp一样
返回值:
    成功:进程组ID
    失败:-1

int setpgid(pid_t pid, pid_t pgid);
功能:
    改变进程默认所属的进程组。通常可用来加入一个现有的进程组或创建一个新进程组。
参数:
    将参1对应的进程,加入参2对应的进程组中
返回值:
    成功:0
    失败:-1

会话

会话概念

会话是一个或多个进程组的集合。会话首进程是创建该新会话的进程,其进程 ID 会成为会话 ID。新进程会继承其父进程的会话 ID。 一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。

  • 一个会话可以有一个控制终端。这通常是终端设备或伪终端设备;
  • 建立与控制终端连接的会话首进程被称为控制进程;当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程。
  • 一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组(前台进程组中的进程以前台运行方式运行;后台进程组中的进程以后台运行方式运行);
  • 如果一个会话有一个控制终端,则它有一个前台进程组,其它进程组为后台进程组;
  • 在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为 后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终 端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。
  • 如果终端接口检测到断开连接,则将挂断信号发送至控制进程(会话首进程)。

进程组和会话:进程组和会话在进程之间形成了一种两级层次关系,进程组是一组相关进程的集合,会话是一组相关进程组的集合。进程组和会话是为支持 shell 作业控制而定义的抽象概念,用户通过 shell 能够交互式地在前台或后台运行命令示例

在某个终端上执行上面两个命令:find / 2 > /dev/null | wc -l &sort < longlist | uniq -c

  • 一开始,打开一个终端,会话的首进程默认是bash(即shell进程),id 为 400。
  • 接着,执行 find / 2 > /dev/null | wc -l & 命令后,创建一个新的进程组,该进程组 id 是 658,进程组中包括 find 进程和 wc 进程,它们的父进程 id 是 400,会话 id 也是 400,且该进程组以后台方式运行,所以是后台进程。
  • 接着,执行 sort < longlist | uniq -c 命令后,创建一个新的进程组,该进程组 id 是 660,进程组中包括 sort 进程和 uniq 进程,它们的父进程和会话 id 也是 400,默认情况下都是运行在前台,前台进程组才享有控制终端的操作权利。

会话相关 API

#include <unistd.h>

pid_t getsid(pid_t pid);
功能:获取进程所属的会话ID
参数:
    pid:进程号,pid为0表示查看当前进程session ID
返回值:
    成功:返回调用进程的会话ID
    失败:-1
#include <unistd.h>

pid_t setsid(void);
功能:
    创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。调用了setsid函数的进程,既是新的会长,也是新的组长。
参数:无
返回值:
    成功:返回调用进程的会话ID
    失败:-1

创建会话注意事项

  1. 调用进程不能是进程组组长,该进程变成新会话首进程(session header),如果该调用进程是组长进程,则出错返回;
  2. 该进程成为一个新进程组的组长进程;
  3. 需有root权限(ubuntu不需要);
  4. 新会话丢弃原有的控制终端,该会话没有控制终端;
  5. 建立新会话时,先调用 fork, 父进程终止,子进程调用 setsid;
  6. 组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(void) {
	pid  pid = -1;
    pid = getsid(0);
    if (-1 == pid) {
        perror("getsid");
        return 1;
    }
    printf("sid:%d\n", pid);
    
    // 创建一个新的回话
    pid = setsid();    // 创建会失败,因为:调用进程不能是进程组组长,该进程变成新会话首进程
    if (-1 == pid) {
        perror("setsid");
        return 1;
    }
	return 0;   
}

守护进程

守护进程介绍

守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。**文件名一般采用以 d 结尾的名字。**守护进程具备下列特征:

  • 生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭。
  • 守护进程是个特殊的孤儿进程,它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如 SIGINT、SIGQUIT),以避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。。
  • Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd 等。

守护进程是一种特殊的后台进程,两者并不完全等价(前置知识:[[11_Linux 终端、进程组、会话、守护进程#二、进程组概念|前台与后台进程]]):

  1. 守护进程已经完全脱离终端控制台了,而后台程序并未完全脱离终端(在终端未关闭前还是会往终端输出结果,需要在以 nohup command & 格式运行才能避免影响);
  2. 守护进程在关闭终端控制台时不会受影响,而后台程序会随用户退出而停止,也会随着控制终端的关闭而退出,需要在以 nohup command & 格式运行才能避免影响。
  3. 守护进程的会话组、当前目录、文件描述符都是独立的。后台运行只是终端进行了一次 fork,让程序在后台执行,这些都没改变;

守护进程模型

守护进程的创建步骤如下:

  1. 创建子进程,父进程退出,子进程继续执行(本步骤是必须的步骤)。
    • 目的:子进程行形式上脱离了原有控制终端
    • 为什么fork之后,父进程需要退出:如果父进程不退出,父进程死亡后,终端会显示一个 shell 提示符,想象一下启动一个守护进程之后,在未来的某个时间,突然出现一个shell 提示符会显得非常诡异。
    • 为什么需要在子进程中继续执行,退出父进程:使用fork 会确保子进程不变成一个进程组的首进程(父进程会是进程组的首进程),这样才能子进程才能调用 setsid() 函数(调用进程不能是进程组组长)。
  2. 在子进程中创建新会话(本步骤是必须的步骤)。
    • 使用 setsid() 函数创建新的会话
    • 目的:使子进程完全独立出来,脱离原有控制终端。
      • 子进程创建一个新的会话会产生两个效果:新会话的首进程组,首进程组的进程 id 与子进程的 id 相同 ;对于新创建出的会话,默认情况下没有新的控制终端来连接该会话,那么该会话就没有控制终端。而守护进程就是在后台运行并且不拥有控制终端的进程。创建一个新的会话的目的就是脱离控制终端。新的会话会脱离原先的控制终端,没有控制终端,我们就不能通过键盘、终端产生信号杀死或影响守护进程
      • 为什么需要在子进程中创建会话,因为如果用父进程创建会话,那么新会话的首进程组,首进程组的进程 id 和父进程 id 相同,而父进程与原有会话的首进程组和首进程的id可能相同,这样两个会话id 相同,产生了冲突。
      • 创建新会话后,只是脱离了控制终端,但是该会话依旧有终端。
  3. 重设文件权限掩码(清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限)。
    • 使用 umask() 函数
    • 目的:防止继承的文件创建屏蔽字拒绝某些权限,以增加守护进程灵活性
  4. 修改进程的当前工作目录,通常会改为根目录(\)。
    • 使用 chdir() 函数
    • 目的:防止占用可卸载的文件系统: 因为守护进程会一直运行到操作系统关闭,但是如果当前目录是u盘,那就导致u盘一直无法卸载,所以一般改为根目录,当然也可以换成其它路径。
  5. 关闭文件描述符,即关闭守护进程从其父进程继承而来的所有打开着的文件描述符。
    • 创建新会话后,只是脱离了控制终端,但是该会话依旧有终端。如果不关闭文件描述符,这些文件描述符还是可能往终端读写数据,这样守护进程就无法操作终端。
    • 继承的打开文件不会用到,浪费系统资源,无法卸载:关闭打开的文件,因为如果不关闭,那将无法卸载当前未关闭的文件所在的磁盘。
  6. 关闭文件描述符0、1、2之后,守护进程通常会打开 /dev/null 并使用 dup2() 使所有这些描述符指向这个设备:关闭文件描述符0、1、2之后,如果守护进程依旧有可能有想这三个文件描述符输出数据,但是文件描述符已经关闭,此时就可能出错,所以要将这三个文件描述符输出的数据重定向到 /dev/null 中,/dev/null 中的数据都会被操作系统自动弃掉。
  7. 开始执行守护进程核心工作(本步骤是必须的步骤)

守护进程参考代码

写一个守护进程, 每隔 2s 获取一次系统时间, 将这个时间写入到磁盘文件:

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

void work(int num) {
    // 捕捉到信号之后,获取系统时间,写入磁盘文件
    time_t tm = time(NULL);
    struct tm * loc = localtime(&tm);
    // char buf[1024];

    // sprintf(buf, "%d-%d-%d %d:%d:%d\n",loc->tm_year,loc->tm_mon
    // ,loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);

    // printf("%s\n", buf);

    char * str = asctime(loc);
    int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
    write(fd ,str, strlen(str));
    close(fd);
}

int main() {

    // 1.创建子进程,退出父进程
    pid_t pid = fork();
    if(pid > 0) {
        exit(0);
    }

    // 2.将子进程重新创建一个会话
    setsid();

    // 3.设置掩码
    umask(022);

    // 4.更改工作目录
    chdir("/home/nowcoder/");

    // 5. 关闭、重定向文件描述符
    int fd = open("/dev/null", O_RDWR);
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);
	// 重定向如果不关闭关闭,就会输出到终端,但是终端提示符依旧可以出现并操作(新的会话,脱离了终端)。重定向关闭,就不会输出到终端。
    
    // 6.业务逻辑

    // 捕捉定时信号
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = work;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, NULL);

    struct itimerval val;
    val.it_value.tv_sec = 2;
    val.it_value.tv_usec = 0;
    val.it_interval.tv_sec = 2;
    val.it_interval.tv_usec = 0;

    // 创建定时器
    setitimer(ITIMER_REAL, &val, NULL);

    // 不让进程结束
    while(1) {
        sleep(10);
    }

    return 0;
}

守护进程相关 API

#include <unistd.h>
int daemon(int nochdir, int noclose);
功能:创建一个守护进程

参数:
	nochdir:=0 将当前目录更改至“/”
	noclose:=0 将标准输入、标准输出、标准错误重定向至“/dev/null”

返回值:
	成功:0
	失败:-1

参考文章

参考文章

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值