glibc 知:手册28:任务控制

本文详细介绍了任务控制在shell中的概念、控制进程终端、访问控制、孤立进程组等,通过示例阐述了如何在shell中实现任务管理功能,包括数据结构、初始化、任务启动、前台后台切换及停止与终止操作。

1. 前言

The GNU C Library Reference Manual for version 2.35

2. 任务控制

Job Control

任务控制是指允许用户在单个登录会话中在多个进程组(或任务)之间移动的协议。设置了任务控制设施,以便大多数程序的适当行为自动发生,并且它们不需要对任务控制做任何特殊的事情。因此,除非您正在编写 shell 或登录程序,否则您可能会忽略本章中的内容。

您需要熟悉与流程创建(请参阅流程创建概念)和信号处理(请参阅信号处理)相关的概念,才能理解本章介绍的材料。

一些旧系统不支持任务控制,但 GNU 系统始终支持,并且它是 POSIX.1 的 2001 修订版中的必需功能(请参阅 POSIX(便携式操作系统接口))。如果需要移植到旧系统,可以使用 _POSIX_JOB_CONTROL 宏在编译时测试系统是否支持任务控制。请参阅整体系统选项

2.1. 任务控制的概念

Concepts of Job Control

交互式 shell 的基本目的是从用户终端读取命令并创建进程来执行这些命令指定的程序。它可以使用 fork(请参阅创建进程)和 exec(请参阅执行文件)函数来执行此操作。

一个命令可能只运行一个进程,但通常一个命令使用多个进程。如果您在 shell 命令中使用“|”运算符,则您在自己的进程中显式请求多个程序。但即使你只运行一个程序,它也可以在内部使用多个进程。例如,一个单一的编译命令,如“cc -c foo.c”通常使用四个进程(尽管通常在任何给定时间只有两个)。如果你运行 make,它的工作就是在不同的进程中运行其他程序。

属于单个命令的进程称为进程组或任务。这样您就可以一次对所有这些进行操作。例如,键入 C-c 会发送信号 SIGINT 以终止前台进程组中的所有进程。

会话是更大的一组进程。通常,源自一次登录的所有进程都属于同一个会话。

每个进程都属于一个进程组。当一个进程被创建时,它成为与其父进程相同的进程组和会话的成员。您可以使用 setpgid 函数将它放在另一个进程组中,前提是该进程组属于同一个会话。

将进程置于不同会话中的唯一方法是使用 setsid 函数使其成为新会话或会话领导者的初始进程。这也会将会话负责人放入一个新的进程组中,并且您不能再次将其移出该进程组。

通常,新会话由系统登录程序创建,会话领导者是运行用户登录 shell 的进程。

支持任务控制的外壳必须安排控制哪个任务可以随时使用终端。否则可能会有多个任务试图一次从终端读取,并且对于哪个进程应该接收用户输入的输入感到困惑。为了防止这种情况,shell 必须使用本章描述的协议与终端驱动程序配合。

shell 一次只能允许一个进程组无限制地访问控制终端。这称为该控制终端上的前台任务。由 shell 管理的其他进程组在没有对终端的这种访问权限的情况下执行,称为后台任务。

如果后台任务需要从其控制终端读取,则由终端驱动程序停止;如果设置了 TOSTOP 模式,同样用于写入。用户可以通过键入 SUSP 字符(参见特殊字符)来停止前台任务,并且程序可以通过向其发送 SIGSTOP 信号来停止任何任务。shell 负责通知任务何时停止,通知用户它们,并提供允许用户以交互方式继续停止的任务并在前台和后台之间切换任务的机制。

有关控制终端的 I/O 的更多信息,请参阅访问控制终端

2.2. 控制进程的终端

Controlling Terminal of a Process

进程的属性之一是它的控制终端。使用 fork 创建的子进程从其父进程继承控制终端。这样,一个会话中的所有进程都从会话领导者那里继承了控制终端。控制终端的会话领导者称为该终端的控制进程。

您通常不需要担心用于将控制终端分配给会话的确切机制,因为它是在您登录时由系统为您完成的。

当单个进程调用 setid 成为新会话的领导者时,它会从其控制终端断开连接。请参阅进程组函数

2.3. 访问控制终端

Access to the Controlling Terminal

控制终端的前台任务中的进程可以不受限制地访问该终端;后台进程没有。本节更详细地描述了当后台任务中的进程尝试访问其控制终端时会发生什么。

当后台任务中的进程尝试从其控制终端读取数据时,通常会向进程组发送一个 SIGTTIN 信号。这通常会导致该组中的所有进程停止(除非它们处理信号并且不会自行停止)。但是,如果读取过程忽略或阻止此信号,则读取将失败并出现 EIO 错误。

同样,当后台任务中的进程尝试写入其控制终端时,默认行为是向进程组发送 SIGTTOU 信号。但是,该行为由本地模式标志的 TOSTOP 位修改(请参阅本地模式)。如果未设置此位(这是默认设置),则始终允许写入控制终端而不发送信号。如果写入过程忽略或阻止 SIGTTOU 信号,则也允许写入。

程序可以执行的大多数其他终端操作都被视为读取或写入。(每个操作的描述应该说明是哪个。)

有关基元读取和写入函数的更多信息,请参阅输入和输出基元。

2.4. 孤立的进程组

Orphaned Process Groups

当一个控制进程终止时,它的终端变得空闲并且可以在它上面建立一个新的会话。(事实上,另一个用户可以在终端上登录。)如果旧会话中的任何进程仍在尝试使用该终端,这可能会导致问题。

为了防止出现问题,即使在会话领导者终止后仍继续运行的进程组被标记为孤立的进程组。

当一个进程组成为孤儿时,它的进程会收到一个 SIGHUP 信号。通常,这会导致进程终止。但是,如果程序忽略此信号或为其建立处理程序(请参阅信号处理),即使在其控制进程终止后,它仍可以像在孤立进程组中一样继续运行;但它仍然无法再访问终端。

2.5. 实现任务控制 shell

Implementing a Job Control Shell

本节通过提供一个广泛的示例程序来说明所涉及的概念,描述了 shell 必须做什么来实现任务控制。

2.5.1. Shell 的数据结构

Data Structures for the Shell

本章中包含的所有程序示例都是简单 shell 程序的一部分。本节介绍整个示例中使用的数据结构和实用函数。

示例 shell 主要处理两种数据结构。任务类型包含有关任务的信息,任务是一组通过管道链接在一起的子流程。进程类型保存有关单个子进程的信息。以下是相关的数据结构声明:

/* A process is a single process.  */
typedef struct process
{
  struct process *next;       /* next process in pipeline */
  char **argv;                /* for exec */
  pid_t pid;                  /* process ID */
  char completed;             /* true if process has completed */
  char stopped;               /* true if process has stopped */
  int status;                 /* reported status value */
} process;

/* A job is a pipeline of processes.  */
typedef struct job
{
  struct job *next;           /* next active job */
  char *command;              /* command line, used for messages */
  process *first_process;     /* list of processes in this job */
  pid_t pgid;                 /* process group ID */
  char notified;              /* true if user told about stopped job */
  struct termios tmodes;      /* saved terminal modes */
  int stdin, stdout, stderr;  /* standard i/o channels */
} job;

/* The active jobs are linked into a list.  This is its head.   */
job *first_job = NULL;

以下是一些用于对任务对象进行操作的实用程序函数。

/* Find the active job with the indicated pgid.  */
job *
find_job (pid_t pgid)
{
  job *j;

  for (j = first_job; j; j = j->next)
    if (j->pgid == pgid)
      return j;
  return NULL;
}

/* Return true if all processes in the job have stopped or completed.  */
int
job_is_stopped (job *j)
{
  process *p;

  for (p = j->first_process; p; p = p->next)
    if (!p->completed && !p->stopped)
      return 0;
  return 1;
}

/* Return true if all processes in the job have completed.  */
int
job_is_completed (job *j)
{
  process *p;

  for (p = j->first_process; p; p = p->next)
    if (!p->completed)
      return 0;
  return 1;
}

2.5.2. 初始化 shell

Initializing the Shell

当一个通常执行任务控制的 shell 程序启动时,它必须小心,以防它被另一个已经在执行自己的任务控制的 shell 调用。

交互运行的子shell 必须确保它已被其父shell 置于前台,然后才能启用任务控制。它通过使用 getpgrp 函数获取其初始进程组 ID 并将其与与其控制终端关联的当前前台任务的进程组 ID 进行比较来实现此目的(可以使用 tcgetpgrp 函数检索)。

如果 subshel​​l 没有作为前台任务运行,它必须通过向自己的进程组发送 SIGTTIN 信号来停止自己。它不能随意把自己放到前台;它必须等待用户告诉父 shell 执行此操作。如果子shell 再次继续,它应该重复检查并在它仍然不在前台时再次自行停止。

一旦子 shell 被其父 shell 放置到前台,它就可以启用自己的任务控制。它通过调用 setpgid 将自己放入自己的进程组,然后调用 tcsetpgrp 将此进程组置于前台来实现这一点。

当 shell 启用任务控制时,它应该将自己设置为忽略所有任务控制停止信号,这样它就不会意外停止自己。您可以通过将所有停止信号的操作设置为 SIG_IGN 来做到这一点。

以非交互方式运行的子shell 不能也不应该支持任务控制。它必须将它创建的所有进程留在与 shell 本身相同的进程组中;这允许非交互式 shell 及其子进程被父 shell 视为单个任务。这很容易做到——只是不要使用任何任务控制原语——但你必须记住让 shell 来做。

下面是示例 shell 的初始化代码,展示了如何执行所有这些操作。

/* Keep track of attributes of the shell.  */

#include <sys/types.h>
#include <termios.h>
#include <unistd.h>

pid_t shell_pgid;
struct termios shell_tmodes;
int shell_terminal;
int shell_is_interactive;


/* Make sure the shell is running interactively as the foreground job
   before proceeding. */

void
init_shell ()
{

  /* See if we are running interactively.  */
  shell_terminal = STDIN_FILENO;
  shell_is_interactive = isatty (shell_terminal);

  if (shell_is_interactive)
    {
      /* Loop until we are in the foreground.  */
      while (tcgetpgrp (shell_terminal) != (shell_pgid = getpgrp ()))
        kill (- shell_pgid, SIGTTIN);

      /* Ignore interactive and job-control signals.  */
      signal (SIGINT, SIG_IGN);
      signal (SIGQUIT, SIG_IGN);
      signal (SIGTSTP, SIG_IGN);
      signal (SIGTTIN, SIG_IGN);
      signal (SIGTTOU, SIG_IGN);
      signal (SIGCHLD, SIG_IGN);

      /* Put ourselves in our own process group.  */
      shell_pgid = getpid ();
      if (setpgid (shell_pgid, shell_pgid) < 0)
        {
          perror ("Couldn't put the shell in its own process group");
          exit (1);
        }

      /* Grab control of the terminal.  */
      tcsetpgrp (shell_terminal, shell_pgid);

      /* Save default terminal attributes for shell.  */
      tcgetattr (shell_terminal, &shell_tmodes);
    }
}

2.5.3. 启动任务

Launching Jobs

一旦 shell 负责在其控制终端上执行任务控制,它就可以启动任务以响应用户键入的命令。

要在进程组中创建进程,您可以使用进程创建概念中描述的相同 fork 和 exec 函数。但是,由于涉及多个子进程,因此事情会稍微复杂一些,您必须小心以正确的顺序执行操作。否则,可能会导致恶劣的竞争条件。

对于如何构建进程之间的父子关系树,您有两种选择。您可以使进程组中的所有进程成为 shell 进程的子进程,也可以使组中的一个进程成为该组中所有其他进程的祖先。本章介绍的示例 shell 程序使用第一种方法,因为它使簿记更加简单。

由于每个进程都被分叉,它应该通过调用 setpgid 将自己放入新的进程组中;请参阅进程组函数。新组中的第一个进程成为其进程组领导,其进程 ID 成为该组的进程组 ID。

shell 还应该调用 setpgid 将它的每个子进程放入新的进程组。这是因为存在一个潜在的时序问题:每个子进程在开始执行新程序之前必须被放入进程组中,而 shell 依赖于在继续执行之前让组中的所有子进程。如果子进程和 shell 都调用 setpgid,这可以确保无论哪个进程先到达它都会发生正确的事情。

如果任务作为前台任务启动,则还需要使用 tcsetpgrp 将新进程组放入控制终端的前台。同样,这应该由 shell 以及它的每个子进程来完成,以避免竞争条件。

每个子进程应该做的下一件事是重置其信号操作。

在初始化期间,shell 进程将自己设置为忽略任务控制信号;请参阅初始化 Shell。因此,它创建的任何子进程也会通过继承忽略这些信号。这绝对是不可取的,因此每个子进程都应在分叉后立即将这些信号的操作显式设置回 SIG_DFL。

由于 shell 遵循这个约定,应用程序可以假设它们从父进程继承了对这些信号的正确处理。但是每个应用程序都有责任不搞乱停止信号的处理。禁用 SUSP 字符的正常解释的应用程序应该为用户提供一些其他机制来停止任务。当用户调用这个机制时,程序应该向进程的进程组发送一个 SIGTSTP 信号,而不仅仅是进程本身。请参阅向另一个进程发送信号

最后,每个子进程都应该以正常方式调用 exec。这也是应该处理标准输入和输出通道的重定向的地方。有关如何执行此操作的说明,请参阅复制描述符

这是示例 shell 程序中负责启动程序的函数。该函数在被 shell 派生后立即由每个子进程执行,并且永远不会返回。

void
launch_process (process *p, pid_t pgid,
                int infile, int outfile, int errfile,
                int foreground)
{
  pid_t pid;

  if (shell_is_interactive)
    {
      /* Put the process into the process group and give the process group
         the terminal, if appropriate.
         This has to be done both by the shell and in the individual
         child processes because of potential race conditions.  */
      pid = getpid ();
      if (pgid == 0) pgid = pid;
      setpgid (pid, pgid);
      if (foreground)
        tcsetpgrp (shell_terminal, pgid);

      /* Set the handling for job control signals back to the default.  */
      signal (SIGINT, SIG_DFL);
      signal (SIGQUIT, SIG_DFL);
      signal (SIGTSTP, SIG_DFL);
      signal (SIGTTIN, SIG_DFL);
      signal (SIGTTOU, SIG_DFL);
      signal (SIGCHLD, SIG_DFL);
    }

  /* Set the standard input/output channels of the new process.  */
  if (infile != STDIN_FILENO)
    {
      dup2 (infile, STDIN_FILENO);
      close (infile);
    }
  if (outfile != STDOUT_FILENO)
    {
      dup2 (outfile, STDOUT_FILENO);
      close (outfile);
    }
  if (errfile != STDERR_FILENO)
    {
      dup2 (errfile, STDERR_FILENO);
      close (errfile);
    }

  /* Exec the new process.  Make sure we exit.  */
  execvp (p->argv[0], p->argv);
  perror ("execvp");
  exit (1);
}

如果 shell 没有以交互方式运行,则此函数不会对进程组或信号执行任何操作。请记住,不执行任务控制的 shell 必须将其所有子进程与 shell 本身保持在同一进程组中。

接下来,这里是实际启动完整任务的函数。创建子进程后,该函数调用其他一些函数将新创建的任务放到前台或后台;这些在前台和后台中讨论。

void
launch_job (job *j, int foreground)
{
  process *p;
  pid_t pid;
  int mypipe[2], infile, outfile;

  infile = j->stdin;
  for (p = j->first_process; p; p = p->next)
    {
      /* Set up pipes, if necessary.  */
      if (p->next)
        {
          if (pipe (mypipe) < 0)
            {
              perror ("pipe");
              exit (1);
            }
          outfile = mypipe[1];
        }
      else
        outfile = j->stdout;

      /* Fork the child processes.  */
      pid = fork ();
      if (pid == 0)
        /* This is the child process.  */
        launch_process (p, j->pgid, infile,
                        outfile, j->stderr, foreground);
      else if (pid < 0)
        {
          /* The fork failed.  */
          perror ("fork");
          exit (1);
        }
      else
        {
          /* This is the parent process.  */
          p->pid = pid;
          if (shell_is_interactive)
            {
              if (!j->pgid)
                j->pgid = pid;
              setpgid (pid, j->pgid);
            }
        }

      /* Clean up after pipes.  */
      if (infile != j->stdin)
        close (infile);
      if (outfile != j->stdout)
        close (outfile);
      infile = mypipe[0];
    }

  format_job_info (j, "launched");

  if (!shell_is_interactive)
    wait_for_job (j);
  else if (foreground)
    put_job_in_foreground (j, 0);
  else
    put_job_in_background (j, 0);
}

2.5.4. 前台和后台

Foreground and Background

现在让我们考虑一下当 shell 将任务启动到前台时必须采取哪些操作,以及这与启动后台任务时必须执行的操作有何不同。

启动前台任务时,shell 必须首先通过调用 tcsetpgrp 使其访问控制终端。然后,shell 应该等待该进程组中的进程终止或停止。这在已停止和已终止的任务中进行了更详细的讨论。

当组中的所有进程都已完成或停止时,shell 应通过再次调用 tcsetpgrp 重新获得对其自己进程组的终端控制权。由于来自后台进程的 I/O 或用户键入的 SUSP 字符导致的停止信号被发送到进程组,因此任务中的所有进程通常会一起停止。

前台任务可能使终端处于奇怪的状态,因此 shell 应在继续之前恢复其自己保存的终端模式。如果任务只是停止,shell 应该首先保存当前的终端模式,以便以后在任务继续时恢复它们。处理终端模式的函数有 tcgetattr 和 tcsetattr;这些在终端模式中进行了描述。

这是执行所有这些的示例 shell 的函数。

/* Put job j in the foreground.  If cont is nonzero,
   restore the saved terminal modes and send the process group a
   SIGCONT signal to wake it up before we block.  */

void
put_job_in_foreground (job *j, int cont)
{
  /* Put the job into the foreground.  */
  tcsetpgrp (shell_terminal, j->pgid);

  /* Send the job a continue signal, if necessary.  */
  if (cont)
    {
      tcsetattr (shell_terminal, TCSADRAIN, &j->tmodes);
      if (kill (- j->pgid, SIGCONT) < 0)
        perror ("kill (SIGCONT)");
    }

  /* Wait for it to report.  */
  wait_for_job (j);

  /* Put the shell back in the foreground.  */
  tcsetpgrp (shell_terminal, shell_pgid);

  /* Restore the shell’s terminal modes.  */
  tcgetattr (shell_terminal, &j->tmodes);
  tcsetattr (shell_terminal, TCSADRAIN, &shell_tmodes);
}

如果进程组作为后台任务启动,shell 应保持在前台本身并继续从终端读取命令。

在示例 shell 中,将任务放入后台不需要做太多事情。这是它使用的函数:

/* Put a job in the background.  If the cont argument is true, send
   the process group a SIGCONT signal to wake it up.  */

void
put_job_in_background (job *j, int cont)
{
  /* Send the job a continue signal, if necessary.  */
  if (cont)
    if (kill (-j->pgid, SIGCONT) < 0)
      perror ("kill (SIGCONT)");
}

2.5.5. 停止和终止的任务

Stopped and Terminated Jobs

启动前台进程时,shell 必须阻塞,直到该任务中的所有进程都已终止或停止。它可以通过调用waitpid函数来做到这一点;请参阅进程完成。使用 WUNTRACED 选项,以便为停止的进程和终止的进程报告状态。

shell 还必须检查后台任务的状态,以便向用户报告已终止和停止的任务;这可以通过使用 WNOHANG 选项调用 waitpid 来完成。对已终止和停止的任务进行此类检查的好地方是在提示输入新命令之前。

shell 还可以通过为 SIGCHLD 信号建立处理程序来接收异步通知,告知子进程有可用的状态信息。请参阅信号处理

在示例 shell 程序中,SIGCHLD 信号通常被忽略。这是为了避免涉及 shell 操作的全局数据结构的重入问题。但是在 shell 不使用这些数据结构的特定时间——例如当它在终端上等待输入时——启用 SIGCHLD 的处理程序是有意义的。用于执行同步状态检查的同一函数(在本例中为 do_job_notification)也可以在此处理程序中调用。

以下是示例 shell 程序中处理检查任务状态并将信息报告给用户的部分。

/* Store the status of the process pid that was returned by waitpid.
   Return 0 if all went well, nonzero otherwise.  */

int
mark_process_status (pid_t pid, int status)
{
  job *j;
  process *p;

  if (pid > 0)
    {
      /* Update the record for the process.  */
      for (j = first_job; j; j = j->next)
        for (p = j->first_process; p; p = p->next)
          if (p->pid == pid)
            {
              p->status = status;
              if (WIFSTOPPED (status))
                p->stopped = 1;
              else
                {
                  p->completed = 1;
                  if (WIFSIGNALED (status))
                    fprintf (stderr, "%d: Terminated by signal %d.\n",
                             (int) pid, WTERMSIG (p->status));
                }
              return 0;
             }
      fprintf (stderr, "No child process %d.\n", pid);
      return -1;
    }
  else if (pid == 0 || errno == ECHILD)
    /* No processes ready to report.  */
    return -1;
  else {
    /* Other weird errors.  */
    perror ("waitpid");
    return -1;
  }
}

/* Check for processes that have status information available,
   without blocking.  */

void
update_status (void)
{
  int status;
  pid_t pid;

  do
    pid = waitpid (WAIT_ANY, &status, WUNTRACED|WNOHANG);
  while (!mark_process_status (pid, status));
}

/* Check for processes that have status information available,
   blocking until all processes in the given job have reported.  */

void
wait_for_job (job *j)
{
  int status;
  pid_t pid;

  do
    pid = waitpid (WAIT_ANY, &status, WUNTRACED);
  while (!mark_process_status (pid, status)
         && !job_is_stopped (j)
         && !job_is_completed (j));
}

/* Format information about job status for the user to look at.  */

void
format_job_info (job *j, const char *status)
{
  fprintf (stderr, "%ld (%s): %s\n", (long)j->pgid, status, j->command);
}

/* Notify the user about stopped or terminated jobs.
   Delete terminated jobs from the active job list.  */

void
do_job_notification (void)
{
  job *j, *jlast, *jnext;

  /* Update status information for child processes.  */
  update_status ();

  jlast = NULL;
  for (j = first_job; j; j = jnext)
    {
      jnext = j->next;

      /* If all processes have completed, tell the user the job has
         completed and delete it from the list of active jobs.  */
      if (job_is_completed (j)) {
        format_job_info (j, "completed");
        if (jlast)
          jlast->next = jnext;
        else
          first_job = jnext;
        free_job (j);
      }

      /* Notify the user about stopped jobs,
         marking them so that we won’t do this more than once.  */
      else if (job_is_stopped (j) && !j->notified) {
        format_job_info (j, "stopped");
        j->notified = 1;
        jlast = j;
      }

      /* Don’t say anything about jobs that are still running.  */
      else
        jlast = j;
    }
}

2.5.6. 继续停止的任务

Continuing Stopped Jobs

shell 可以通过向其进程组发送 SIGCONT 信号来继续停止的任务。如果任务在前台继续,shell 应首先调用 tcsetpgrp 以使任务访问终端,并恢复保存的终端设置。在前台继续任务后,shell 应该等待任务停止或完成,就好像任务刚刚在前台启动一样。

示例 shell 程序使用相同的函数对 put_job_in_foreground 和 put_job_in_background 处理新创建的和继续的任务。这些函数的定义在前台和后台中给出。继续停止的任务时,将传递一个非零值作为 cont 参数,以确保发送 SIGCONT 信号并根据需要重置终端模式。

这只留下了一个函数,用于更新 shell 内部关于正在继续的工作的簿记:

/* Mark a stopped job J as being running again.  */

void
mark_job_as_running (job *j)
{
  Process *p;

  for (p = j->first_process; p; p = p->next)
    p->stopped = 0;
  j->notified = 0;
}

/* Continue the job J.  */

void
continue_job (job *j, int foreground)
{
  mark_job_as_running (j);
  if (foreground)
    put_job_in_foreground (j, 1);
  else
    put_job_in_background (j, 1);
}

2.5.7. 丢失的部分

The Missing Pieces

本章中包含的示例 shell 的代码摘录只是整个 shell 程序的一部分。特别是,对于如何分配和初始化任务和程序数据结构,完全没有提及。

大多数真实的 shell 提供了一个复杂的用户界面,它支持命令语言。变量;文件名的缩写、替换和模式匹配;之类的。所有这些都太复杂了,无法在这里解释!相反,我们专注于展示如何实现可以从此类 shell 调用的核心流程创建和任务控制功能。

下表总结了我们提出的主要入口点:

void init_shell (void)

初始化 shell 的内部状态。请参阅初始化 Shell

void launch_job (job *j, int foreground)

将任务 j 作为前台任务或后台任务启动。请参阅启动任务

void do_job_notification (void)

检查并报告任何已终止或停止的任务。可以同步调用,也可以在 SIGCHLD 信号的处理程序中调用。请参阅已停止和终止的任务

void continue_job (job *j, int foreground)

继续工作 j。请参阅继续停止的任务

当然,真正的 shell 也希望提供其他功能来管理任务。例如,拥有列出所有活动任务或向任务发送信号(例如 SIGKILL)的命令会很有用。

2.6. 任务控制函数

本节包含与任务控制相关的功能的详细说明。

2.6.1. 识别控制终端

Identifying the Controlling Terminal

您可以使用 ctermid 函数获取可用于打开控制终端的文件名。在 GNU C 库中,它始终返回相同的字符串:“/dev/tty”。这是一个特殊的“神奇”文件名,它指的是当前进程的控制终端(如果有的话)。要查找特定终端设备的名称,请使用 ttyname;请参阅识别终端

函数 ctermid 在头文件 stdio.h 中声明。

函数:char * ctermid (char *string)

Preliminary: | MT-Safe !posix/!string | AS-Safe | AC-Safe | See POSIX Safety Concepts.

ctermid 函数返回一个字符串,其中包含当前进程的控制终端的文件名。如果 string 不是空指针,它应该是一个至少可以容纳 L_ctermid 个字符的数组;字符串在此数组中返回。否则,将返回指向静态区域中的字符串的指针,该指针可能会在后续调用此函数时被覆盖。

如果由于任何原因无法确定文件名,则返回空字符串。即使返回文件名,也不能保证访问它所代表的文件。

宏:int L_ctermid

该宏的值是一个整数常量表达式,表示字符串的大小,足以容纳 ctermid 返回的文件名。

另请参阅识别终端中的 isatty 和 ttyname 函数。

2.6.2. 进程组函数

Process Group Functions

以下是用于操作进程组的函数的描述。您的程序应包含头文件 sys/types.h 和 unistd.h 以使用这些功能。

函数:pid_t setsid (void)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

setsid 函数创建一个新会话。调用进程成为会话领导者,并被放入一个新的进程组,其进程组 ID 与该进程的进程 ID 相同。新进程组中最初没有其他进程,新会话中也没有其他进程组。

该功能还使调用进程没有控制终端。

如果成功,setsid 函数返回调用进程的新进程组 ID。返回值 -1 表示错误。为此函数定义了以下 errno 错误条件:

EPERM

调用进程已经是进程组组长,或者周围已经有另一个进程组具有相同的进程组 ID。

函数:pid_t getsid (pid_t pid)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

getsid 函数返回指定进程的会话负责人的进程组 ID。如果 pid 为 0,则返回当前进程的会话负责人的进程组 ID。

如果出现错误,则返回 -1 并设置 errno。为此函数定义了以下 errno 错误条件:

ESRCH

没有具有给定进程 ID pid 的进程。

EPERM

调用进程和pid指定的进程在不同的会话中,实现不允许从调用进程访问ID为pid的进程的会话组长的进程组ID。

函数:pid_t getpgrp (void)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

getpgrp 函数返回调用进程的进程组 ID。

函数:int getpgid (pid_t pid)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

getpgid 函数返回进程 pid 的进程组 ID。您可以为 pid 参数提供值 0 以获取有关调用进程的信息。

如果出现错误,则返回 -1 并设置 errno。为此函数定义了以下 errno 错误条件:

ESRCH

没有具有给定进程 ID pid 的进程。调用进程和pid指定的进程在不同的会话中,实现不允许从调用进程访问ID为pid的进程的进程组ID。

函数:int setpgid (pid_t pid, pid_t pgid)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

setpgid 函数将进程 pid 放入进程组 pgid。作为一种特殊情况,pid 或 pgid 都可以为零来表示调用进程的进程 ID。

如果操作成功,setpgid 返回零。否则返回-1。为此函数定义了以下 errno 错误条件:

EACCES

以 pid 命名的子进程在被分叉后执行了一个 exec 函数。

EINVAL

pgid 的值无效。

ENOSYS

系统不支持任务控制。

EPERM

pid 参数指示的进程是会话领导者,或与调用进程不在同一会话中,或 pgid 参数的值与调用进程在同一会话中的进程组 ID 不匹配。

ESRCH

pid 参数指示的进程不是调用进程或调用进程的子进程。

函数:int setpgrp (pid_t pid, pid_t pgid)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

这是 setpgid 的 BSD Unix 名称。这两个函数的作用完全相同。

2.6.3. 控制终端访问的函数

Functions for Controlling Terminal Access

这些是读取或设置终端前台进程组的函数。您应该在应用程序中包含头文件 sys/types.h 和 unistd.h 以使用这些功能。

尽管这些函数采用文件描述符参数来指定终端设备,但前台任务与终端文件本身相关联,而不是与特定的打开文件描述符相关联。

函数:pid_t tcgetpgrp (int filedes)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

此函数返回与在描述符文件上打开的终端关联的前台进程组的进程组 ID。

如果没有前台进程组,则返回值是一个大于 1 的数字,与任何现有进程组的进程组 ID 都不匹配。如果以前是前台任务的任务中的所有进程都已终止,并且尚未将其他任务移至前台,则可能会发生这种情况。

如果发生错误,则返回值 -1。为此函数定义了以下 errno 错误条件:

EBADF

filedes 参数不是有效的文件描述符。

ENOSYS

系统不支持任务控制。

ENOTTY

与filedes参数关联的终端文件不是调用进程的控制终端。

函数:int tcsetpgrp (int filedes, pid_t pgid)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

该函数用于设置终端的前台进程组 ID。参数filedes是指定终端的描述符;pgid 指定进程组。调用进程必须是与 pgid 相同的会话的成员,并且必须具有相同的控制终端。

出于终端访问目的,此函数被视为输出。如果从其控制终端上的后台进程调用它,通常会向进程组中的所有进程发送 SIGTTOU 信号。例外情况是调用进程本身忽略或阻塞 SIGTTOU 信号,在这种情况下执行操作并且不发送信号。

如果成功,则 tcsetpgrp 返回 0。返回值 -1 表示错误。为此函数定义了以下 errno 错误条件:

EBADF

filedes 参数不是有效的文件描述符。

EINVAL

pgid 参数无效。

ENOSYS

系统不支持任务控制。

ENOTTY

文件不是调用进程的控制终端。

EPERM

pgid 不是与调用进程在同一会话中的进程组。

函数:pid_t tcgetsid (int fildes)

Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.

该函数用于获取fildes指定的终端为控制终端的会话的进程组ID。如果调用成功,则返回组 ID。否则返回值为 (pid_t) -1 并且全局变量 errno 设置为以下值:

EBADF

filedes 参数不是有效的文件描述符。

ENOTTY

调用进程没有控制终端,或者文件不是控制终端。

3. 参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

canpool

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值