UNIX环境高级编程 学习笔记 第十三章 守护进程

守护进程的生存期长,它们常常在系统引导时启动,仅在系统关闭时才终止,大多都以root权限运行,它们没有控制终端,在后台运行。

如果在基于BSD的系统上执行:

ps -axj

-a选项显示由其他用户拥有的进程状态;-x选项显示没有控制终端的进程状态;-j选项显示与作业有关的信息(如会话ID、进程组ID、控制终端名),基于System V的系统中,与此相类似的命令为ps -efj。为提高安全性,某些UNIX系统不允许用户使用ps命令查看不属于自己的进程。以上命令输出为:
在这里插入图片描述
从左到右每列含义依次为:用户ID、进程ID、父进程ID、进程组ID、会话ID、终端名称、命令字符串。(以上ps命令在支持会话ID的系统上运行;在一些基于BSD的系统,如Mac OS X 10.6.8上,将打印进程所属进程组的session结构的地址而非会话ID)

父进程ID为0的进程通常是内核进程,它们作为系统引导过程的一部分启动(init进程例外,它是内核在引导过程中启动的用户层命令)。内核进程通常存在于系统的整个生命期中,以root权限运行,无控制终端,无命令行。

以上ps的输出实例中,内核守护进程的名字出现在方括号中,该版本的Linux使用名为kthreadd的特殊内核进程创建其他内核进程,所以kthreadd是其他内核进程的父进程。

对于需要使用进程形式来执行工作且不会被用户层进程调用的内核组件通常有自己的守护进程,如Linux中:
1.kswapd守护进程也称内存换页守护进程,它支持虚拟内存子系统在经过一段时间后将脏页面慢慢写回磁盘来回收这些页面。

2.flush守护进程可将内存达到阈值时将脏页面冲洗至磁盘,它也定期将脏页面冲洗回磁盘来减少系统出现故障时发生的数据丢失。每个写回的设备都有一个冲洗守护进程,多个冲洗守护进程可以共存。

3.sync_supers守护进程定期将文件系统元数据冲洗至磁盘。

4.jbd守护进程帮助实现了ext4文件系统的日志功能。

通常init进程的进程ID为1(Mac OS X中是launchd),它是一个系统守护进程,主要负责启动各运行层次特定的系统服务,这些服务通常是在它们自己的守护进程的帮助下实现的。

rpcbind守护进程提供将远程过程调用程序号映射为网络端口号的服务。rsyslogd守护进程可被由管理员允许的任何程序使用,这些程序将其消息记入系统日志,这些日志可在控制台上打印出来,也可将其写入一个文件中。

inetd守护进程侦听网络接口,以便取得来自网络的对各种网络服务进程的请求。守护进程nfsd、nfsiod、lockd、rpciod、rpc.idmapd、rpc.statd、rpc.mountd提供对网络文件系统的支持(前4个是内核守护进程,后3个是用户级守护进程)。

cron守护进程在定期安排的日期和时间执行命令。atd守护进程与cron守护进程类似,但它对于每个任务只执行一次,而非定期反复执行。cupsd守护进程是打印假脱机进程,它处理系统提出的各个打印请求。sshd守护进程提供了安全的远程登录和执行设施。

大多守护进程都以root特权运行。守护进程没有控制终端,其终端名设置为问号。内核守护进程以无控制终端方式启动,而用户层守护进程缺少控制终端可能是守护进程调用了setsid的结果,大多数用户层守护进程都是进程组的组长进程以及会话首进程,而且是这些进程组和会话中的唯一进程(rsyslogd是个例外)。用户层守护进程的父进程通常是init进程,因为通常它真正的父进程在fork出守护进程后就退出了。

编写守护进程的规则:
1.调用umask将文件模式创建屏蔽字设为一个已知值(通常是0),因为由继承得来的文件模式创建屏蔽字可能会被设置为拒绝某些权限。

2.守护进程的父进程调用fork,然后使父进程exit,这样可保证:
(1)如果该守护进程作为一条简单的shell命令启动,则父进程终止会让shell认为这条命令已经执行完毕。

(2)子进程一定不是一个进程组的组长进程,这是即将调用的setsid函数(用来创建新会话)的先决条件。

(3)调用setsid创建一个新会话,该进程就成为新会话的首进程、新进程组的组长进程,且没有控制终端。(有人建议在基于System V的系统中,此时再调用一次fork,然后终止父进程,在子进程中运行守护进程,这样保证守护进程不是会话首进程(在System V中,当会话首进程打开第一个尚未与一个会话相关联的终端设备时,只要在调用open时没有指定O_NOCTTY标志,System V派生的系统将此终端作为控制终端分配给会话),可防止守护进程获取控制终端;System V中另一种防止守护进程获取控制终端的方式是,无论何时打开控制终端,都要指定O_NOCTTY)。

(4)将当前工作目录改为根目录,由于守护进程的当前工作目录可能在一个挂载文件系统中,且守护进程通常在系统再引导前一直存在,这将导致该文件系统不能被卸载。或者,某些守护进程将当前工作目录更改到某个指定位置,在此位置进行它们的全部工作(如行式打印机假脱机进程可能将其工作目录更改到它们的spool目录上)。

(5)关闭不再需要的文件描述符,使得守护进程不再持有从其父进程继承来的任何文件描述符。可使用open_max函数或getrlimit函数获取最大文件描述符值,然后关闭直到该值的所有文件描述符。

(6)某些守护进程打开/dev/null使其具有文件描述符0、1、2,这样任何试图读写标准输入、标准输出、标准错误的库例程都不会产生任何效果。这样守护进程就不再与任何终端设备关联,因此其输出无处显示,也不能从交互式用户那里接收输入,即使守护进程是从交互式会话启动的,但守护进程在后台新会话中运行,所以登录会话的终止并不影响守护进程。

想要初始化成为守护进程的程序可调用以下函数:

#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/stat.h>

void daemonize(const char *cmd) {
    int i, fd0, fd1, fd2;
    pid_t pid;
    struct rlimit rl;
    struct sigaction sa;

    // clear file creation mask
    umask(0);

    // get maximun number of file descriptors
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0) {
        printf("%s: can't get file limit\n", cmd);
		exit(1);
    }

    // become a session leader to lose controlling TTY
    if ((pid = fork()) < 0) {
        printf("%s: can't fork\n", cmd);
		exit(1);
    } else if (pid != 0) {
        exit(0);
    }
    setsid();

    // ensure future opens won't allocate controlling TTYS
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0) {
        printf("%s: can't ignore SIGHUP\n", cmd);
		exit(1);
    }
    // 通过再次调用fork使子进程不是会话首进程,从而避免在基于System V的系统中,不以O_NOCTTY标志打开终端时使进程获得控制终端
    if ((pid = fork()) < 0) {
        printf("%s: can't fork\n");
		exit(1);
    } else if (pid != 0) {
        exit(0);
    }

    // change the current working directory to the root so we won't prevent file systems from being unmounted
    if (chdir("/") < 0) {
        printf("%s: can't change directory to /\n", cmd);
		exit(1);
    }

    // close all open file descriptors
    if (rl.rlim_max == RLIM_INFINITY) {
        rl.rlim_max = 1024;
    }
    for (i = 0; i < rl.rlim_max; ++i) {
        close(i);
    }

    // attach file descriptors 0, 1 and 2 to /dev/null
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);

    // initialize the log file
    openlog(cmd, LOG_CONS, LOG_DAEMON);
    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        syslog(LOG_ERR, "unexpected file descriptors $d $d $d", fd0, fd1, fd2);
		exit(1);
    }
}

若以上daemonize函数被main直接调用,然后main进入休眠状态,可用ps命令检查该守护进程的状态:
在这里插入图片描述
可见没有进程的ID是13799,即守护进程在一个孤儿进程组中,且它不是会话首进程,因此没有机会通过调用open分配到一个控制终端。

守护进程的出错消息需要一个设施集中存放,自4.2 BSD以来,BSD的syslog设施被大多数守护进程使用。

从BSD派生的很多系统都支持syslog。在SVR 4(System V Release 4)之前,System V中没有集中的守护进程记录设施。SUS的XSI扩展中包含了syslog函数。
在这里插入图片描述
由上图,有三种产生日志的方法:
1.内核例程调用log函数,任何用户进程都可通过open和read设备/dev/klog来读取这些消息。

2.大多用户进程(守护进程)调用syslog函数产生日志,日志被发送至UNIX域数据报套接字/dev/log。

3.无论用户进程是在此主机上,还是通过TCP/IP网络连接到此主机的其他主机上,都可将日志发到UDP端口514。

syslogd守护进程读取所有以上3种格式的日志,syslogd启动时读一个配置文件(通常是/etc/syslog.conf),该文件决定了不同种类的消息应送向何处(如紧急消息可发送至系统管理员,并在控制终端上打印;警告消息可发送至一个文件)。

BSD的syslog设施的接口:
在这里插入图片描述
调用openlog是可选的,如不调用,则第一次调用syslog时会自动调用openlog。调用closelog也是可选的,它只是关闭曾被用于与syslogd守护进程通信的描述符。

调用openlog使我们可以指定一个ident参数,此参数将被加到每条日志消息中,ident参数一般是程序名。option参数可指定各种选项,选项内容如下,如果SUS的openlog函数支持此选项,则在XSI列中用黑点表示:
在这里插入图片描述
facility参数目的是让配置文件说明设施类型,来自不同设施类型的消息以不同的方式进行处理。如果该参数传0,或不调用openlog,那么在调用syslog时,可将facility作为priority参数的一部分进行说明。facility参数可选值如下,SUS只定义了facility参数可选值的一个子集:
在这里插入图片描述
调用syslog可产生一条日志,priority参数是facility和level参数的组合(使用按位或运算符|组合),level参数取值如下,从上到下优先级逐渐降低:
在这里插入图片描述
format参数类似于函数printf的参数,其中每个%m都也会被替换为与当前errno值对应的出错消息字符串。

setlogmask函数用于设置进程的记录优先级屏蔽字,返回值为调用它之前的屏蔽字。当设置了记录优先级屏蔽字时,如果一条消息的优先级没有在优先级屏蔽字中,它将不会被记录。将记录优先级屏蔽字设为0没有任何效果。

很多系统提供logger命令来向syslog设施发送日志,一些实现为logger命令设置了facility、level、ident等选项,但SUS没有为logger命令定义任何选项。logger命令为以下目的而设计:用于以非交互式运行的shell脚本,在shell脚本需要产生日志时使用。

很多平台提供一种syslog的变体来处理可变参数列表:
在这里插入图片描述
SUS中不包含以上变体,如使用需要功能测试宏,如在FreeBSD上的__BSD_VISIBLE或Linux上的__USE_BSD。

大多数syslog函数实现将消息短时间存放在队列中,如果此段时间内有重复消息到达,则会打印一条类似“上条消息重复了N次”的消息。

某些守护进程实现为,在任一时刻只运行该守护进程的一个副本,如cron守护进程,如果同时有多个实例运行,则每个副本都可能试图开始某个预定的操作,造成该操作的重复执行;或守护进程要访问某个设备,但该设备只能排他地访问。

使用文件和记录锁可保证一个守护进程只有一个副本在运行,方法是守护进程创建一个有固定名字的文件,并在这个文件整体上加一把写锁,在此之后创建写锁的尝试都会失败。如果守护进程终止,写锁自动删除,这简化了复原所需的处理。

使用文件和记录锁来保证只运行守护进程的一个副本:

#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <syslog.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <sys/stat.h>

#define LOCKFILE "/var/run/daemon.pid"
#define LOCKMODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

extern int lockfile(int);

int already_running(void) {
    int fd;
    char buf[16];

    fd = open(LOCKFILE, O_RDWR | O_CREAT, LOCKMODE);
    if (fd < 0) {
        syslog(LOG_ERR, "can't open %s: %s", LOCKFILE, strerror(errno));
		exit(1);
    }
    if (lockfile(fd) < 0) {
        if (errno == EACCES || errno == EAGAIN) {
		    close(fd);
		    return 1;
		}
		syslog(LOG_ERR, "can't lock %s: %s", LOCKFILE, strerror(errno));
		exit(1);
	}
    ftruncate(fd, 0);    // 把文件截断到长度0,即完全截断文件
    sprintf(buf, "%ld", (long)getpid());
    write(fd, buf, strlen(buf) + 1);
    return 0;
}

以上函数会被每个守护进程的副本运行,它们都将试图创建一个文件,并将其进程ID写入该文件,使管理员易于标识此进程。如果文件已经加锁,则lockfile函数将失败,如果errno被设为EACCES或EAGAIN,表示该守护进程已经在运行,以上函数返回1;否则将文件长度截断为0(需要截断文件的原因在于,之前守护进程实例的进程ID位数可能长于当前进程的进程ID,如之前的守护进程ID为12345,而新实例的进程ID是9999,则当前进程将进程ID写入文件后,文件中留下的是99995),并将进程ID写入文件,之后返回0。

UNIX中,守护进程的通用惯例:
1.若守护进程使用锁文件,则该文件通常放于/var/run目录,守护进程可能需要超级用户权限才能在此目录下创建文件。锁文件的名字通常是name.pid,name是该守护进程或服务的名字,如cron守护进程锁文件的名字是/var/run/crond.pid。

2.若守护进程支持配置选项,则配置文件通常放于/etc目录。配置文件名通常是name.cfg,如syslogd守护进程的配置文件通常是/etc/syslog.conf。

3.守护进程可用命令行启动,但通常是由系统初始化脚本之一(/etc/rc或/etc/init.d/)启动的。守护进程终止时,应自动重启它,在System V风格的init命令系统中,可在/etc/inittab中为该守护进程加一个respawn项。

4.若守护进程有一个配置文件,当该守护进程启动时会读该文件,在此之后不再查看它,如果修改了配置文件,则该守护进程需要被停止,然后再启动,使被更改的配置生效。某些守护进程为避免重启动,会捕捉SIGHUP信号,当它们接收到此信号时,重新读配置文件,由于守护进程不与终端结合,它们或者是无控制终端的会话首进程,或者是孤儿进程组中的成员,所以守护进程没有理由期望接收SIGHUP,因此守护进程可安全重复使用SIGHUP。

守护进程通过接收信号重新读配置文件的例子:

#include <pthread.h>
#include <syslog.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>

sigset_t mask;

extern int already_running(void);

void reread(void) { /* ... */ }

void *thr_fn(void *arg) {
    int err, signo;

    for ( ; ;) {
        err = sigwait(&mask, &signo);
		if (err != 0) {
		    syslog(LOG_ERR, "sigwait failed");
		    exit(1);
		}
	
		switch (signo) {
		case SIGHUP:
		    syslog(LOG_INFO, "Re-reading configuration file");
		    reread();
		    break;
		
		case SIGTERM:
		    syslog(LOG_INFO, "got SIGTERM; exiting");
		    exit(0);
	
		default:
		    syslog(LOG_INFO, "unexpected signal %d\n", signo);
	    }
    }
    return 0;
}

int main(int argc, char *argv[]) {
    int err;
    pthread_t tid;
    char *cmd;
    struct sigaction sa;

    if ((cmd = strrchr(argv[0], '/')) == NULL) {
        cmd = argv[0];
    } else {
        cmd++;
    }

    // become a daemon
    daemonize(cmd);

    // make sure only one copy of the daemon is running
    if (already_running()) {
        syslog(LOG_ERR, "daemon already running");
        exit(1);
    }

    // restore SIGHUP default and block all signals
    // SIGHUP信号已在daemonize函数中被忽略
    sa.sa_handler = SIG_DFL;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0) {
        printf("can't restore SIGHUP default");
		exit(1);
    }
    sigfillset(&mask);
    if ((err = pthread_sigmask(SIG_BLOCK, &mask, NULL)) != 0) {
        printf("SIG_BLOCK error");
		exit(1);
    }

    // create a thread to handle SIGHUP and SIGTERM
    err = pthread_create(&tid, NULL, thr_fn, 0);
    if (err != 0) {
        printf("can't create thread");
		exit(1);
    }

    // proceed with the rest of the daemon
    
    exit(0);
}

以上程序调用了daemonize函数初始化守护进程,从该函数返回后,调用了already_running函数以确保该守护进程只有一个副本在运行,到这一点时,SIGHUP信号仍被忽略(在daemonize函数中忽略的),需要恢复信号的系统默认处理方式,否则调用sigwait的线程见不到该信号。

处理信号过程为:阻塞所有信号,然后创建一个线程处理信号,该线程唯一的工作就是等待并处理信号。

SIGHUP和SIGTERM信号的默认动作是终止进程,但除了信号处理线程外,其余线程都阻塞了该信号,因此信号只会发送到信号处理线程,之后信号处理线程中的sigwait函数返回接收到的信号并处理,守护进程不会被终止。

单线程守护进程捕捉SIGHUP并重读其配置文件的例子:

#include <syslog.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>

extern int lockfile(int);
extern int already_running(void);

void reread(void) { /* ... */ }

void sigterm(int signo) {
    syslog(LOG_INFO, "got SIGTERM; exiting");
    exit(0);
}

void sighup(int signo) {
    syslog(LOG_INFO, "Re-reading configuration file");
    reread();
}

int main(int argc, char *argv[]) {
    char *cmd;
    struct sigaction sa;

    if ((cmd = strrchr(argv[0], '/')) == NULL) {
        cmd = argv[0];
    } else {
        cmd++;
    }

    // become a daemon
    daemonized(cmd);

    // make sure only one copy of the daemon is running
    if (already_running()) {
        syslog(LOG_ERR, "daemon already running");
		exit(1);
    }

    // handle signals of interest
    sa.sa_handler = sigterm;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGHUP);
    sa.sa_flags = 0;
    if (sigaction(SIGTERM, &sa, NULL) < 0) {
        syslog(LOG_ERR, "can't catch SIGTERM: %s", strerror(errno));
		exit(1);
    }
    sa.sa_handler = sighup;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGTERM);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0) {
        syslog(LOG_ERR, "can't catch SIGHUP: %s", strerror(errno));
		exit(1);
    }

    // proceed with the rest of the daemon

    exit(0);
}

以上程序中,可以将重读逻辑放在信号处理程序中,也可以在信号处理程序中设置一个标志,并由守护进程的主线程完成所有工作。

守护进程常用作服务器进程,如syslogd进程可称为服务器进程,而用户进程用UNIX域数据报套接字向其发送消息。一般,服务器进程等待客户进程与其联系,提出某种类型的服务器要求,syslogd服务器进程提供的服务是将一条出错消息记录到日志文件中。以上过程中客户进程和服务器进程之间的通信是单向的。

服务器进程调用fork然后exec另一个程序来向客户提供服务是常见的,fork出来提供服务的服务器子进程通常管理着许多文件描述符,包括通信端点、配置文件、日志文件、其他文件,这些子进程中的文件描述符打开并无大碍,因为它们很可能不会被在子进程中执行的程序使用,最坏情况下,保持这些文件描述符的打开会导致安全问题,被执行的程序可能有恶意行为,如更改服务器配置文件或欺骗客户端程序使其认为正在与服务器端通信,从而获取未授权信息。解决以上问题的一个简单方法是对所有被执行程序不需要的文件描述符设置执行时关闭标志:

#include <fcntl.h>

int set_cloexec(int fd) {
    int val;

    if ((val = fcntl(fd, F_GETFD, 0)) < 0) {
        return -1;
    }

    val |= FD_CLOEXEC;    // enable close-on-exec
    
    return fcntl(fd, F_SETFD, val);
}

直接调用openlog或第一次调用syslog时,会打开用于UNIX域数据报套接字的特殊设备文件/dev/log,如果调用openlog前,守护进程先调用了chroot(改变进程的根目录,使进程只能访问指定的目录中的内容),进程就不能打开/dev/log。因此守护进程在调用chroot前,需要调用选项为LOG_NDELAY的openlog,它会立即打开此特殊设备文件,并生成一个描述符,之后即使调用了chroot,该描述符依然有效。ftpd就是这样,为了安全调用了chroot,但它仍可以调用syslog对出错条件进行记录。

有些守护进程不是会话首进程,只有会话首进程才有机会通过调用open函数获取控制终端。

进程调用daemonize函数后,失去了控制终端,此时再调用getlogin(此函数打印运行进程的登录用户名),打印其结果:

#include <stdio.h>
#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/stat.h>

void daemonize(const char *cmd) {
    int i, fd0, fd1, fd2;
    pid_t pid;
    struct rlimit rl;
    struct sigaction sa;

    // clear file creation mask
    umask(0);

    // get maximun number of file descriptors
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0) {
        printf("%s: can't get file limit\n", cmd);
		exit(1);
    }

    // become a session leader to lose controlling TTY
    if ((pid = fork()) < 0) {
        printf("%s: can't fork\n", cmd);
		exit(1);
    } else if (pid != 0) {
        exit(0);
    }
    setsid();

    // ensure future opens won't allocate controlling TTYS
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0) {
        printf("%s: can't ignore SIGHUP\n", cmd);
		exit(1);
    }
    if ((pid = fork()) < 0) {
        printf("%s: can't fork\n");
		exit(1);
    } else if (pid != 0) {
        exit(0);
    }

    // change the current working directory to the root so we won't prevent file systems from being unmounted
    if (chdir("/") < 0) {
        printf("%s: can't change directory to /\n", cmd);
		exit(1);
    }

    // close all open file descriptors
    if (rl.rlim_max == RLIM_INFINITY) {
        rl.rlim_max = 1024;
    }
    for (i = 0; i < rl.rlim_max; ++i) {
        close(i);
    }

    // attach file descriptors 0, 1 and 2 to /dev/null
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);

    // initialize the log file
    openlog(cmd, LOG_CONS, LOG_DAEMON);
    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        syslog(LOG_ERR, "unexpected file descriptors $d $d $d", fd0, fd1, fd2);
		exit(1);
    }
}

int main() {
    FILE *fp;
    char *p;

    daemonize("getlog");
    p = getlogin();
    fp = fopen("/tmp/getlog.out", "w");
    if (fp != NULL) {
        if (p == NULL) {
		    fprintf(fp, "no login name\n");
		} else {
		    fprintf(fp, "login name: %s\n", p);
		}
    }
    exit(0);
}

运行它:
在这里插入图片描述
以上函数的运行结果依赖于不同的系统实现,daemonize函数会关闭所有打开文件描述符,然后打开文件描述符0、1、2到/dev/null,这意味着进程不再有控制终端,所以getlogin函数不能在utmp文件(其中含登录到系统的用户)中看到进程的登录项,在Linux 3.2.0和Solaris 10中,会输出“no login name”;但在FreeBSD 8.0和Mac OS X 10.6.8中,登录名是由进程表维护的,且在调用fork时复制,即除非其父进程没有登录名(如系统自引导时调用init),否则进程总能获得其登录名。

总结一个stackoverflow上的问题,问题链接https://stackoverflow.com/questions/32384148/why-does-running-a-background-task-over-ssh-fail-if-a-pseudo-tty-is-allocated。

提问者这样这样执行命令可以创建目标文件:

[bob@server ~]$ ssh localhost 'touch foobar &'
[bob@server ~]$ ls foobar
foobar

但如果加上ssh的-t选项,目标文件就会创建失败:

[bob@server ~]$ ssh -t localhost 'touch foobar &'
Connection to localhost closed.
[bob@server ~]$ echo $?
0
[bob@server ~]$ ls foobar
ls: cannot access foobar: No such file or directory

首先ssh命令会在对端用-c选项启动bash,即进入非交互模式,此模式下,作业控制是关闭的,因此不论是否加&符号,对端shell都不会创建一个新的进程组来运行touch命令,而是shell和touch命令在同一进程组中,因为作业控制关闭状态下不需要切换前后台进程组。

其次&有一个效果,使得shell不会等待该进程运行结束,而是直接返回0。

当不加-t选项时,对端shell是由对端sshd fork+创建新会话+exec的,由于sshd是一个守护进程,因此对端shell也是没有控制终端的,由于有控制终端的会话首进程终止会向前台进程组中的所有进程发送SIGHUP信号,但没有控制终端,前台进程组也就无从谈起,因此就不会发送SIGHUP信号给touch命令。

当加ssh的-t选项时,会以交互方式在对端运行shell,导致ssh创建一个伪终端,并且对端shell的控制终端是这个伪终端,且会话首进程就是对端shell,当用&运行命令时,伪终端的slave端(会exec shell)会不等待后台命令运行,直接退出,而有控制终端的会话首进程终止会向前台进程组中的所有进程发送SIGHUP信号,导致touch命令终止。

对以上问题,即使使用了nohup也没用,因为当shell fork出子进程运行touch时,shell进程的退出和子进程设置SIGHUP的信号处理之间存在竞争关系,导致nohup不一定生效。

解决方式:

ssh -t localhost 'set -m ; touch foobar &'

set -m命令强制打开终端的作业控制,使得touch命令进入后台进程组运行。

另一个解决方式:

ssh -t localhost 'touch foobar & wait `pgrep touch`'

此命令会先执行touch命令,然后等待touch命令结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值