linux系统编程之进程(五):终端、作业控制与守护进程

一、终端的概念

在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal),控制终端是保存在PCB中的信息,而我们知道fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctrl-C表示SIGINT,Ctrl-\表示SIGQUIT。

每个进程都可以通过一个特殊的设备文件/dev/tty访问它的控制终端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty提供了一个通用的接口,一个进程要访问它的控制终端既可以通过/dev/tty也可以通过该终端设备所对应的设备文件来访问。ttyname函数可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不能是任意文件。在linux上的命令tty 也可以查看到当前的终端。

比如我们在图形界面下打开一个终端可能是/dev/pts/0, 第二个可能是/dev/pts/1 ...(网络终端,比如通过xshell/putty 等方式登录机器)

而切换到字符界面下可能是/dev/tty1 ...(虚拟终端,直接外设输命令)

二、作业控制

事实上,Shell分前后台来控制的不是进程而是作业(Job)或者进程组(Process Group)。一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell可以同时运行一个前台作业和任意多个后台作业,这称为作业控制(Job Control)。例如用以下命令启动5个进程(这个例子出自APUE): $ proc1 | proc2 &

$ proc3 | proc4 | proc5

其中proc1和proc2属于同一个后台进程组,proc3、proc4、proc5属于同一个前台进程组,Shell进程本身属于一个单独的进程组。这些进程组的控制终端相同,它们属于同一个Session,一个Session与一个控制终端相关。当用户在控制终端输入特殊的控制键(例如Ctrl-C)时,内核会发送相应的信号(例如SIGINT)给前台进程组的所有进程。各进程、进程组、Session的关系如下图所示。

在上面的例子中,proc3、proc4、proc5被Shell放到同一个前台进程组,其中有一个进程是该进程组的Leader,Shell调用wait等待它们运行结束。一旦它们全部运行结束,Shell就调用tcsetpgrp 函数将自己提到前台继续接受命令。但是注意,如果proc3、proc4、proc5中的某个进程又fork出子进程,子进程也属于同一进程组,但是Shell并不知道子进程的存在,也不会调用wait等待它结束。换句话说,proc3 | proc4 | proc5是Shell的作业,而这个子进程不是,这是作业和进程组在概念上的区别。一旦作业运行结束,Shell就把自己提到前台,如果原来的前台进程组还存在(如果这个子进程还没终止),则它自动变成后台进程,被init进程接管。

三、守护进程

守护进程是在后台运行不受终端控制的进程,通常情况下守护进程在系统启动时自动运行,用户关闭终端窗口或注销也不会影响守护进程的运行,只能kill掉。守护进程的名称通常以d结尾,比如sshd、xinetd、crond等

我们用ps axj 命令查看系统中的进程,凡是TPGID(前台进程组ID)一栏写着-1的都是没有控制终端的进程,或者TTY一栏为?的,也就是守护进程。

实际上一般的进程(前后台) 在关闭终端窗口后,会收到 SIGHUP 信号导致中断,可以使用 nohup command  args > /dev/null 2>&1 & 来忽略 hangup 信号,或者直接使用 setsid command args 来使进程成为守护进程。需要注意的是,使用 nohup 时的父进程id 为终端的进程id,使用 setsid 时的父进程id 为 1(即 init 进程 id)。

四、创建守护进程步骤

调用fork(),创建新进程,它会是将来的守护进程 在父进程中调用exit,保证子进程不是进程组组长 调用setsid创建新的会话期 将当前目录改为根目录 将标准输入、标准输出、标准错误重定向到/dev/null

成功调用setsid函数的结果是:

创建一个新的Session,当前进程成为Session Leader,当前进程的id就是Session的id。

创建一个新的进程组,当前进程成为进程组的Leader,当前进程的id就是进程组的id。

如果当前进程原本有一个控制终端,则它失去这个控制终端,成为一个没有控制终端的进程。

示例程序:

/*************************************************************************
    > File Name: process_.c
    > Author: Simba
    > Mail: dameng34@163.com
    > Created Time: Sat 23 Feb 2013 02:34:02 PM CST
 ************************************************************************/
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

int setup_daemon(int, int);
/* 守护进程一直在后台运行且无控制终端 */
int main(int argc, char *argv[])
{
    //  daemon(0, 0)
    setup_daemon(0, 0);
    printf("test ... \n"); // 无输出
    for(;;) ;
    return 0;
}

int setup_daemon(int nochdir, int noclose)
{
    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork error");
    if (pid > 0)
        exit(EXIT_SUCCESS);
    /* 调用setsid的进程不能为进程组组长,故fork之后将父进程退出 */
    setsid(); // 子进程调用后生成一个新的会话期
    if (nochdir == 0)
        chdir("/"); //更改当前目录为根目录
    if (noclose == 0)
    {
        int i;
        for (i = 0; i < 3; ++i)
            close(i);
        open("/dev/null", O_RDWR); // 将标准输入,标准输出等都重定向到/dev/null
        dup(0);
        dup(0);
    }

    return 0;
}

执行程序再ps axj 一下:

simba@ubuntu:~/Documents/code/linux_programming/APUE/process$ ./daemon  simba@ubuntu:~/Documents/code/linux_programming/APUE/process$ ps axj  PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND

...........................................................................................................................

 1  7678  7678  7678 ?           -1 Rs    1000   0:03 ./daemon

可以看出守护进程的ID也是进程组的ID,也是会话期的ID,此外这个会话期没有前台进程组。

五、使用daemon函数实现守护进程

功能:创建一个守护进程

原型:int daemon(int nochdir, int noclose); 参数: nochdir:=0将当前目录更改至“/” noclose:=0将标准输入、标准输出、标准错误重定向至“/dev/null”

注:也有一些说法,表示daemon 实现是fork 2 次,具体可以google fork 2 times daemon,据说主要是为了避免子进程的僵尸进程问题。

int daemon()
{
    int  fd;
    pid_t pid;
    if((pid = fork()) != 0)
    {
        exit(0);
  }
  
  setsid();
  
    signal(SIGINT, SIG_IGN);
    signal(SIGHUP, SIG_IGN);
    signal(SIGQUIT, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGTTOU, SIG_IGN);
    signal(SIGTTIN, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);
    signal(SIGTERM, SIG_IGN);
    
    struct sigaction sig;
    sig.sa_handler = SIG_IGN;
    sig.sa_flags = 0;
    sigemptyset(&sig.sa_mask);
    sigaction(SIGPIPE,&sig,NULL);
    
    umask(0);
    
    if((pid = fork()) != 0)
    {
        exit(0);
    }
   
    fd = open("/dev/null", O_RDWR);
    dup2(fd, STDIN_FILENO)    
    dup2(fd, STDOUT_FILENO)
    return 0;
}

参考:《linux c 编程一站式学习》、《APUE》

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Petrichor的专栏

pycharm: 调用自定义库 报"Unresolved reference"

有时候当我们调用自定义的库时,会发生报”Unresolved reference”警告:

1062
来自专栏哲学驱动设计

发布 Rafy .NET Standard 版本 Nuget 包

去年年中,Rafy 框架的源码就已经支持了 Net Standard 2.0 版本。其开源代码也已经上传到 Github 中:https://github.co...

1183
来自专栏转载gongluck的CSDN博客

python笔记:#003#PyCharm 的初始设置

PyCharm 的初始设置(知道) 目标 恢复 PyCharm 的初始设置 第一次启动 PyCharm 新建一个 Python 项目 设置 PyCharm 的字...

4056
来自专栏张善友的专栏

升级个人网站框架组件IBatisNet+Castle

今天晚上花了两个小时从Castle网站拿到最新的Castle.Facilities.IBatisNetIntegration代码,将IBatisNet版本升级到...

2077
来自专栏C++

python笔记:#003#PyCharm 的初始设置

932
来自专栏张善友的专栏

【实验手册】使用Visual Studio Code 开发.NET Core应用程序

2169
来自专栏张善友的专栏

【实验手册】使用Visual Studio Code 开发.NET Core应用程序

.NET Core with Visual Studio Code 目录 概述... 2 先决条件... 2 练习1: 安装和配置.NET Core以及Visu...

2359
来自专栏python3

python自动化脚本nginx_status

运维自动化,已经成为运维必不可少的一部分,下面附上自己写的监控nginx_status脚本,大神轻喷

601
来自专栏文渊之博

如何升级PowerShell

背景: 开发的PowerShell 脚本需要使用Invoke-RestMethod命令,发现在老的服务器上不支持这一命令,经过查询得知由于PS版本的问题。涉及到...

57910
来自专栏happyJared

Chocolatey,Windows下的包管理器

Chocolatey(中文译:巧克力味)是Windows平台下的一款包管理工具,类似于Linux平台的apt-get和yum。第一次接触到Chocolatey...

1983

扫码关注云+社区