linux下的session leader及controlling terminal

最近在调试嵌入式下设备启动基于busybox的init程序所执行的rc.sysinit脚本时,发现该脚本执行的后台脚本,会因为没有关联到串口设备(通过ps的TTY一列可以确认) ,导致我们做了tty设备检测的程序无法顺利执行,借此机会,查阅了busybox的init代码,并对linux的session group和controlling terminal有了一定的理解。

在libc的info文档中Job Control一节,有如下几个重要的定义:

  • process group,即进程组,用管道符创建的一堆命令就行成了一个进程组。
  • session,即会话,通常从login shell登陆后的所有进程都属于一个session,即session由login shell创建,注意libc的文档还是从传统的语义来讲的,对于现代的大多数桌面环境而言(DE,如KDE/GNOME等),一个图形会话,往往会通过setsid函数起大量的session),一个session下可以管理多个process group,一个进程可以在同一个session下的不同process group中迁移,但将一个进程移到另外的会话中则只能通过setsid这个函数来创建新的会话实现。
  • session leader,即会话leader,创建会话的进程称为会话leader,从上面的描述可知,login shell一般为会话leader,首次调用setsid的进程也将成为session leader。
  • controlling terminal,即控制终端,进程的重要属性之一,由fork创建的子进程继续父进程的controlling terminal,而从setsid的libc文档中可以看出(或者用man 3 setsid查看),setsid调用后,进程将做为新的会话的会话leader,并且丢失controlling terminal属性,而其后该会话leader打开的首个tty设备,将成为该会话的controlling terminal(见附2说明);shell通常只会将controlling terminal这个属性给予一个进程组,以便由这个进程组通过终端设备获取输入或者输出信息,这组进程将称为前台任务,而未获得输入或输出等权限的进程组,在尝试向controlling terminal读取或写入数据时,将收到SIGTTIN或SIGTTOU信号,默认情况下这个信号将中止相关程序的执行。这点是合理的,因为如果不做此限制,后台程序很可能扰乱终端输出或者输入的处理。

经过上述讲述,以代码形式给出上述概念的演示。

#include "stdlib.h"
#include "errno.h"
#include "unistd.h"
#include "fcntl.h"

int main()
{
	int err, fd;
	pid_t pid;
	char *pts_name;

    pid = fork();
    if (pid != 0) {
        exit(0); // 主进程退出。
	}

    pid = setsid(); // 在子进程中创建新的会话

    // 打印从父进程继续而来的tty,注意,该tty已经不是我们的controlling terminal
	printf("before we prepare the new stdio, the child inherit its parent's stdio %s\n", ttyname(0));

    // 如下一段代码用于创建一个pseudo-terminal,将作为tty设备被新的会话leader打开,成为该会话的controlling terminal
    fd = getpt();
    pts_name = ptsname(fd);
    printf("allocated a new pts is %s\n", pts_name); 
    grantpt(fd);
    unlockpt(fd);

    // 首次open的tty设备将成为controlling terminal
	fd = open(ptsname(fd), O_RDWR);

    // 将该fd做为标准输入,在本例中意义不大
	close(STDIN_FILENO);
	dup2(fd, STDIN_FILENO);

    // 停留2分,以便我们可以通过ps检验是否获得了新的session及controlling terminal
	sleep(120);
}

编译并执行上述程序后,执行后,立即用如下命令,可以查看到新进程确实拥有了新的controlling terminal了。

ps  -u fortitude -o pid,args,tt,sess,ppid

PID COMMAND                     TT        SESS  PPID
8086 ./sessionleader             pts/5     8086     1

参考文档:

  1. libc info,可在shell下info libc,或在emacs下info libc查看
  2. http://uw714doc.sco.com/en/SDK_sysprog/_The_Controlling-Terminal_and_Pr.htmlWhen a session-leader without a controlling-terminal opens a terminal-device-file and the flag O_NOCTTY is clear on open, that terminal becomes the controlling-terminal assigned to the session-leader if the terminal is not already assigned to some session (see open(2)). When any process other than a session-leader opens a terminal-device-file, or the flag O_NOCTTY is set on open, that terminal does not become the controlling-terminal assigned to the calling-process.

谈谈Linux下的时间机制

最近同事在解决一个时区问题时花费了较多时间,但仍是一头雾水,通过查询Linux相关的文档,我给出了他一些修改建议,经过验证后问题得以修复,事后我总结来看,主要是该同事对于Linux下的时间机制的理解不够深入,因此这里我将相关的知识给记录下来,以解释相关的疑惑。

Linux下目前有三种主要的时钟,其名称及作用如下:

硬件时钟

此时钟一般由主板上的单独电子器件提供,它与CPU核心是独立的运行的,往往在电脑关机时仍能继续运行(想想我们的PC在装机重启后,时间仍然还保持着正确性就是因为这么个东西存在)。

此时钟还有一些其他名称,如RTC(实时时钟)、BIOS时钟或CMOS时钟,此时钟可以通过hwclock这样的小工具来设置。

需要注意的是,硬件时钟中是不感知时区的,其硬件实现记录了个初始值(如我司设备经常采用的1970.1.1等),然后用户可以通过hwclock写入一个时间来更新它,我们可以认为它是一个有初始值的计数器,用户可以更新当前的值,之后它将按照其计时精度来更新其值,供系统通过硬件接口读取。

系统时钟

此时钟是由Linux内核所维护的一个软件时钟,由时钟中断触发更新,其时间是相对于1970.1.1-00:00:00 UTC这个时间的秒数(UTC与GMT即格林威治时间意义相同,为世界标准时间),为方便这里的讲述,我们可以假定我们在格林威治这个地方讨论时间,这样我们可以先把时区放到一边不管了。

在Linux启动后,系统启动脚本(不管是sysvinit还是systemd都将有这样的处理)将通过hwclock读取硬件时钟,并将时间设置到内核中,此后用户获取时间均通过内核获取。

单调时钟

在Linux系统上,用户可以通过date命令更新系统时间,从而导致系统时钟出现跳变,这对于一些涉及到对调度单元(如协程)运行时间来记时以便切出去让其他进程执行的程序来说则会产生计时不准的问题,为此,Linux上支持了一个所谓的单调时钟的接口,此接口除支持单调时钟外,还支持对于系统时钟的获取,此接口属于较新的POSIX标准,如果Linux版本较老的话,可能将无法支持

此接口的定义如下:

int clock_gettime(clockid_t clk_id, struct timespec *tp); // 采用CLOCK_MONOTONIC这个clk id可以获取到单调时钟

上面讨论时,我刻意互略了关于时区的处理,为简单起见,我们这里可以认为Linux内核是不感知时区的(实际上由于种种原因比如VFAT文件系统的时间戳处理就需要内核需要感知到时区)。

在Linux下,由于大量程序均需要依赖于glibc,因此glibc会读取TZ这个环境变量来设置时区,下面是一些C库函数的简单介绍:

struct tm *gmtime(const time_t *timep); // 获取UTC时间,也就是说怎么改TZ变量或者通过tzset更改时区都不会影响此值
struct tm *localtime(const time_t *timep); // 获取本地时间,更改TZ变量或者通过tzset更改时区都会影响此值

当然当于时区还需要补充一点的是,环境变量这个东西只影响所属的进程,在子进程中修改并不会影响父进程,libc.so甚至提供了一个全局变量用来保存当前进程的时区设置(注意,虽然libc.so为动态库,但其在mmap映射是private的,所以会导致其中定义的全局变量每个进程都复制了一份),如果修改的话,需要通过signal类似的IPC机制通知相关进程予以刷新,这也是我同事改时区反复出问题的一个原因。

从一个编程者的角度来看,如果是涉及到对调度单元(如协程)运行时间记时的话,要尽可能采用单调时钟,下面是syslog-ng项目所采用的异步io库ivykis关于时间处理的代码片段,可以看出,它将尽可能去使用单调时钟或者clock_gettime接口,在最后才尝试使用gettimeofday来获取时间。

#ifdef HAVE_CLOCK_GETTIME
static int clock_source;
#endif

void iv_time_get(struct timespec *time)
{
struct timeval tv;

#if defined(HAVE_CLOCK_GETTIME) && defined(HAVE_CLOCK_MONOTONIC_FAST)
if (clock_source < 1) {
if (clock_gettime(CLOCK_MONOTONIC_FAST, time) >= 0)
return;
clock_source = 1;
}
#endif

#if defined(HAVE_CLOCK_GETTIME) && defined(HAVE_CLOCK_MONOTONIC)
if (clock_source < 2) {
if (clock_gettime(CLOCK_MONOTONIC, time) >= 0)
return;
clock_source = 2;
}
#endif

#if defined(HAVE_CLOCK_GETTIME) && defined(HAVE_CLOCK_REALTIME)
if (clock_source < 3) {
if (clock_gettime(CLOCK_REALTIME, time) >= 0)
return;
clock_source = 3;
}
#endif

gettimeofday(&tv, NULL);
time->tv_sec = tv.tv_sec;
time->tv_nsec = 1000L * tv.tv_usec;
}