【Linux】Linux任务管理与守护进程

简介: 【Linux】Linux任务管理与守护进程

一、任务管理


1、进程组概念

Linux中,每个进程除了有一个进程ID之外,还有一个属性是进程组(PGID),进程组是一个或多个进程的集合

通常,进程组内的所有进程它们与同一作业(任务)相关联,可以接收来自同一终端的各种消息和信号。每个进程组有一个唯一的进程组ID。每个进程组都可以有一个组长进程。组长进程的标识是:其进程组ID等于其进程ID,组长进程可以创建一个进程组,创建该组中的进程。

需要注意的是,只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关,也就是说: 进程组的生命周期从被创建开始,到其内所有进程终止或离开该组

2、作业概念

Shell区分前后台不是按照进程来区分的,而是按照作业(Job)或者进程组(Process Group)。

一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell可以运行一个前台作业和任意多个后台作业,这称为作业控制。

作业与进程组的区别

如果作业中的某个进程又创建了子进程,则子进程不属于作业。一旦作业运行结束,Shell就把自己提到前台(Shell也是一个作业,当其他前台作业在运行时,Shell就会变成后台作业,这就是为什么我们完成其他作业时,无法使用Shell),如果原来的前台进程还存在,也就是这个被创建的子进程还没有终止,那么它将自动变为后台进程组。

3、会话概念

Linux是多用户多任务的分时系统,所以必须要支持多个用户同时使用一个操作系统。当一个用户登录一次系统就形成一次会话,一个会话可包含多个进程组,但只能有一个前台进程组。每个会话都有一个会话首领(leader),即创建会话的进程。


Linux给我们提供了一个系统调用:setsid()调用此函数能创建一个会话。

  • 必须注意的是,只有当前进程不是进程组的组长时,才能创建一个新的会话。调用setsid之后,调用此函数的进程成为新会话的leader。
  • 此函数的返回值是:成功则返回调用进程的ID。当出现错误时,返回 -1,并设置errno来指示错误。

  • 一个会话可以有10个或控制终端(会话的领头进程打开一个终端之后, 该终端就成为该会话的控制终端),在这个会话中的所有进程组都关联这个终端文件。
  • 建立与控制终端连接的会话首进程又被称为控制进程。
  • 一个会话中的几个进程组可被分为一个前台进程组和多个后台进程组,所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意多个后台进程组。

例如,下面我们用运行多个sleep,让它们协同完成我们的任务,则它们应该属于同一个进程组,这些进程组的控制终端相同,它们同属于一个会话,当用户在控制终端输入特殊的控制键(如Ctrl+C产生SIGINT,Ctrl+\产生SIGQUIT,Ctrl+Z产生SIGTSTP),内核就会发送相应的信号给前台进程组中的所有进程。

我们看到了这个进程组的会话id是28977,我们来查看一下,这个会话首领是谁?

结果显示是bash,所以bash是当前会话的会话首领,也是控制进程。当我们用Xshell或是终端登录时,本质都是先创建一个会话,然后启动bash进程,该进程成为组长,所有的命令行启动的任务都是在对应的会话内运行的。

实际我们每一次登录的过程都是新建会话的过程,同一个会话中的所有进程的SID是相同的。

4、相关操作

前台进程&后台进程

直接运行某一可执行程序,此时默认将程序放到前台运行,在前台运行的进程的状态后有一个+号,例如S+

运行可执行程序时在后面加上&,可以指定将程序放到后台运行,在后台运行的进程的状态后没有+号。

我们将程序放到后台运行时会发现多了一行提示信息,例如上述的:

[1] 7560

其中[1]是作业的编号,如果同时运行多个作业可以用这个编号进行区分,7560是该作业中最后一个进程的id(一个作业可以由多个进程组成)。

jobs、fg、bg命令

使用jobs命令,可以查看当前会话当中有哪些作业。

使用fg命令(foreground),可以将某个作业提至前台运行,如果该作业正在后台运行则直接提至前台运行,如果该作业处于停止状态,则给进程组的每个进程发SIGCONT信号使它继续运行并提至前台。

例如,使用fg 1命令将1号作业提到前台运行

使用bg命令,可以让某个停止的作业在后台继续运行(Running),本质就是给该作业的进程组的每个进程发SIGCONT信号。

二、守护进程

守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。

守护进程是一种很有用的进程,Linux的大多数服务器就是用守护进程实现的,守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。

用户登录时创建会话,当用户退出时销毁会话,会话销毁会话内的进程就有可能受到影响,因为我们的服务器进程是要一直运行的而且不能受用户登录和登出的影响,所以服务器要采用守护进程的方式运行!

1、守护进程的创建

守护进程创建的原理是:当我们运行一个服务器程序时,不要在当前用户登录的会话内运行服务器程序,而是创建一个新的会话(利用系统调用setsid()),让这个服务器程序在这个新的会话内运行,这样用户的登录与登出都不会影响我们服务器程序。

守护进程的创建步骤

  1. 设置文件掩码为0,(保证我们后续设置的权限不受权限掩码的影响)。
  2. 忽略异常信号,如:SIGCHLDSIGPIPE(避免不必要的信号干扰服务器的运行)。
  3. fork后终止父进程,子进程创建新会话(保证当前进程不是组长进程)。
  4. 再次fork,终止父进程,保持子进程不是会话首进程,从而保证后续不会再和其他终端相关联

守护进程不能直接和用户交互,也就没有必要再打开某个终端了,而打开一个终端需要你是会话首进程,为了防止守护进程打开终端,我们直接让父进程退出,由于子进程不是会话首进程,也就没有能力打开其他终端了。(这是一种防御性编程,该操作不是必须的)

  1. 更改工作目录为根目录(方便帮我们使用和查找文件)。
  2. 将标准输入、标准输出、标准错误重定向到/dev/null

守护进程不能直接和用户交互,也就是说守护进程已经与终端去关联了,因此一般我们会将守护进程的标准输入、标准输出以及标准错误都重定向到/dev/null/dev/null是一个字符文件(设备),通常用于屏蔽/丢弃输入输出信息。(该操作不是必须的)

从上面的操作中我们也能看出:守护进程本质是孤儿进程

#pragma once
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <fcntl.h>
void Daemon()
{
    // 1. 将权限掩码设置为0
    umask(0);
    // 2.忽略SIGCHILD, SIGPIPE信号
    if (SIG_ERR == signal(SIGCHLD, SIG_IGN))
    {
        std::cerr << "signal fail: " << strerror(errno) << std::endl;
        exit(errno);
    }
    if (SIG_ERR == signal(SIGPIPE, SIG_IGN))
    {
        std::cerr << "signal fail: " << strerror(errno) << std::endl;
        exit(errno);
    }
    // 3. 创建子进程,建立新的会话
    pid_t id = fork();
    if (id < 0)
    {
        std::cerr << "fork fail: " << strerror(errno) << std::endl;
        exit(errno);
    }
    else if (id > 0) exit(0); // 父进程退出
    if (setsid() < 0)
    {
        std::cerr << "setsid fail: " << strerror(errno) << std::endl;
        exit(errno);
    }
    // 4.再次fork,终止父进程,保持子进程不是会话首进程,从而保证后续不会再和其他终端相关联
    if (fork() > 0) exit(0);
    // 5.更改工作目录
    if (chdir("/") < 0)
    {
        std::cerr << "chdir fail: " << strerror(errno) << std::endl;
        exit(errno);
    }
    // 6.将输入输出重定向到/dev/null
    int fd = open("/dev/null", O_RDWR);
    if (fd < 0)
    {
        std::cerr << "open fail: " << strerror(errno) << std::endl;
        exit(errno);
    }
    if (dup2(fd, 0) < 0)
    {
        std::cerr << "dup2 fail: " << strerror(errno) << std::endl;
        exit(errno);
    }
    if (dup2(fd, 1) < 0)
    {
        std::cerr << "dup2 fail: " << strerror(errno) << std::endl;
        exit(errno);
    }
    if (dup2(fd, 2) < 0)
    {
        std::cerr << "dup2 fail: " << strerror(errno) << std::endl;
        exit(errno);
    }
    // 关闭不需要的文件描述符
    close(fd);
}

将此代码加到我们的服务器的启动前,或者一个死循环的程序的运行前,运行程序,我们用ps命令查看该进程信息:

我们发现该进程的TPGID为-1,TTY显示的是?也就意味着该进程已经与终端去关联了。

其次,我们还可以看到该进程的PID与其PGID和SID是不同的,也就是说该进程既不是组长进程也不是会话首进程。

此外,我们还可以看到服务器进程的SID(6069)与bash进程的SID(就是上面的642)是不同的,即它们不属于同一个会话。

通过ls /proc/进程id -al命令,可以看到该进程的工作目录已经成功改为了根目录。

通过ls /proc/进程id/fd -al命令,可以看到该进程的标准输入、标准输出以及标准错误也成功重定向到了/dev/null

2、守护进程的库函数

C语言给我们实现了一个daemon函数用于创建守护进程

daemon函数的函数原型如下:

int daemon(int nochdir, int noclose);

参数说明:

  • 如果参数nochdir为0,则将守护进程的工作目录该为根目录,否则不做处理。
  • 如果参数noclose为0,则将守护进程的标准输入、标准输出以及标准错误重定向到/dev/null,否则不做处理。

运行实例:

#include <unistd.h>
int main()
{
  daemon(0, 0);
  while (1);
  return 0;
}

调用daemon函数创建的守护进程与我们原生创建的守护进程差距不大。区别是:

  • daemon函数创建出来的守护进程,既是组长进程也是会话首进程。
  • daemon函数没有进行信号屏蔽。

在实际使用中我们更加倾向于自己写daemon函数,这样定制性更好一些。

相关文章
|
1月前
|
资源调度 Linux 调度
Linux c/c++之进程基础
这篇文章主要介绍了Linux下C/C++进程的基本概念、组成、模式、运行和状态,以及如何使用系统调用创建和管理进程。
35 0
|
3月前
|
网络协议 Linux
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
637 2
|
3月前
|
Linux Python
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
67 2
|
14天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
44 4
linux进程管理万字详解!!!
|
4天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
41 8
|
13天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
46 4
|
14天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
16天前
|
消息中间件 存储 Linux
|
22天前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
23 1
|
1月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
22 1