进程组是一个或多个进程的集合。通常,它们是在同一作业中结合起来的,同一进程组中的各进程接收来自同一终端的各种信号,每个进程组有一个唯一的进程组ID。每个进程组有一个组长进程,该组长进程的ID等于进程组ID。从进程组创建开始到最后一个进程离开为止的时间称为进程组的生命周期。
#include pid_t getpgrp(void); 返回值:调用进程的进程组ID int setpgid(pid_t pid, pid_t pgid); 返回值:成功,返回0;失败,返回-1 说明: setpgid用于添加进程到一个现有的进程组,或者创建一个新的进程组。 函数将进程ID为pid的进程加入ID为pgid的进程组中。 如果pid == pgid,则pid指定的进程变为进程组长; 如果pid == 0,则使用调用者的进程ID; 如果pgid == 0,则将pid用作进程组ID。 |
会话是一个或者多个进程组的集合。进程组通常是由shell管道编制在一起的,如下:
proc1 | proc2 & proc3 | proc4 | proc5 |
进程调用setsid函数创建一个新的会话:
#include pid_t setsid(void); 返回值:成功,返回进程组ID;失败,返回-1 说明: 如果调用此函的进程不是一个进程组长,则此函数创建一个新的会话,具体如下: (1) 该进程变为新会话的会话首进程。 (2) 该进程成为一个新进程组的组成进程,新进程组ID是该进程ID。 (3) 该进程没有控制终端。
pid_t getsid(pid_t pid); 返回值:成功,返回会话首进程的进程组ID;失败,返回-1
说明: getsid(0)返回调用进程的会话首进程的进程组ID,如果pid不属于调用者所在的会话,则不返回。 |
一个会话可以有一个控制终端,通常是终端设备或伪终端设备。建立与控制终端连接的会话首进程被称为控制进程。一个会话中的进程组可被分为一个前台进程组和一个后台进程组。需要有一种方法通知内核哪一个进程组是前台进程组,这样便于终端设备驱动程序知道将终端输入和终端产生的信号发送到何处:
#include pid_t tcgetpgrp(int fd); 返回值:成功,返回前台进程组ID;失败,返回-1 int tcsetpgrp(int fd, pid_pgrpid); 返回值:成功,返回0;失败,返回-1 说明: 其中,fd为相关联的打开终端。大多数应用程序并不直接调用这两个函数,而是由作业控制shell调用。 需要管理控制终端的应用程序可以调用tcgetsid函数获得控制终端的会话首进程的进程组ID: #include pid_t tcgetsid(int fd); 返回值:成功,返回会话首进程的进程组ID;失败,返回-1 |
作业控制允许在一个终端上启动多个作业(进程组),哪一个作业可以访问该终端以及哪些作业在后台运行。从shell使用作业控制的角度看,用户可以在前台或者后台启动一个作业,例如:
vi main.c在前台启动只有一个进程组成的作业,而make all &在后台启动只有一个进程组成的作业。
我们可以键入3个特殊字符使得终端程序产生信号,并将它们发送到前台进程组:
中断字符(Ctrl + C)产生SIGINT信号;
退出字符(Ctrl + \)产生SIGQUIT信号;
挂起字符(Ctrl + Z)产生SIGTSTP信号;
只有前台作业才可以接收终端上输入的字符,如果后台作业试图都终端,那么终端驱动程序向后台作业发送特定信号SIGTTIN,该信号将停止此后台作业,而shell则向用户发送通知,然后用户就可以利用shell命令fg将此作业转为前台作业运行。但是如果后台作业输出到控制终端又将发生什么呢?我们可以通过stty命令禁止这种情况。此时,终端驱动程序向后台作业发送SIGTTOU信号,使其进程阻塞,当然此时我们也可以利用fg将其移到前台运行。
守护进程(daemon)常常在系统引导装入时启动,在系统关闭时终止。由于守护进程没有控制终端(其终端名设置为?),因此,其在后台运行。大多守护进程都以超级用户权限执行。
(1) 首先调用umask将文件模式创建屏蔽字设置为一个已知数值(通常是0)。这样做是防止继承而来的屏蔽字没有某些权限,尤其是写权限。
(2) 调用fork,然后使父进程exit。
(3) 调用setsid创建一个新会话。
(4) 将当前工作目录更改为根目录。因为守护进程通常在系统引导之前就存在,如果守护进程的当前工作目录在一个需要挂载的文件系统上,那么该文件系统不能被卸载。也有某些守护进程会把当前工作目录更改到某个指定的位置,例如行式打印机假脱机守护进程就可能将其工作目录更改到它们的spool目录上。
(5) 关闭不再需要的文件描述符。可以使用open_max函数或者getrlimit函数获取最高文件描述符值,然后关闭直到该值的所有文件描述符。这样做可以避免守护进程从其父进程继承任何文件描述符。
(6) 某些守护进程将文件描述符0、1、2指向/dev/null。这样可以使得任何以恶搞试图读标准输入、写标准输出、写标准错误输出的程序不产生任何效果。由于守护进程是在后台运行的,因此登录会话的终止并不影响守护进程。如果其他用户在同一终端设备上登录,我们自然不希望在该终端上见到守护进程的输出,用户也不希望他们在终端上的输入被守护进程读取,因此上述措施是相当有用的。
如下程序可由想要初始化为守护进程的程序调用,在main函数中调用函数daemonize,然后使main进程进入休眠状态,通过ps –efj命名查看进程状态,可以发现守护进程init,其终端名为 ?。
[root@benxintuzi process]# cat init.c #include#include #include #include #include #include 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 maximum number of file descriptors. * */ if (getrlimit(RLIMIT_NOFILE, &rl) < 0) { printf("%s: can't get file limit\n", cmd); return ; } /* * * Become a session leader to lose controlling TTY. * */ if ((pid = fork()) < 0) { printf("%s: can't fork\n", cmd); return ; } else if (pid != 0) /* parent */ 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); return ; } if ((pid = fork()) < 0) { printf("%s: can't fork\n", cmd); return ; } else if (pid != 0) /* parent */ 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); } /* * * 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(void) { daemonize("ps"); sleep(30); return 0; } [root@benxintuzi process]# ./init [root@benxintuzi process]# ps -efj UID PID PPID PGID SID C STIME TTY TIME CMD root 1 0 1 1 0 19:35 ? 00:00:03 /sbin/init root 2198 2174 2198 2057 0 19:38 pts/0 00:00:00 vim init.c root 2237 1 2236 2236 0 19:45 ? 00:00:00 ./init root 2238 2188 2238 2100 0 19:45 pts/1 00:00:00 ps -efj