Linux系统编程(四)进程

一、进程的产生(fork)

fork(2) 系统调用会复制调用进程来创建一个子进程,在父进程中 fork 返回子进程的 pid,在子进程中返回 0。

#include 
#include 

pid_t fork(void);

fork 后子进程不继承未决信号和文件锁,资源利用量清 0。 由于进程文件描述符表也继承下来的,所以可以看到父子进程的输入输出指向都是一样的,这个特性可以用于实现基本的父子进程通信。

init() 是所有进程的祖先进程,pid = 1。

例子(fork_test.c)

#include 
#include 
#include 
#include 

int main()
{
  pid_t pid;
  printf("Begin\n");
  //fflush(); //!!!重要

  if ((pid = fork()) == 0) {
    // child
    printf("child process executed\n");
    exit(0);
  } else if (pid < 0) {
    perror("fork");
    exit(1);
  }

  // father
  //sleep(1);
  printf("parent process executed\n");
  exit(0);
}

运行结果

注意:父子进程的运行顺序不能确定,由调度器的调度策略决定。 

面试题:当将输出重定向到文件里面时,Begin 为什么打印了两次?如下图:

答案:输出到终端默认是行缓冲模式,加 “\n” 即可刷新缓冲区,但由于重定向到文件是写文件,而写文件是全缓冲,所以 “\n” 无法刷新缓冲区,所以需要在 Begin 后加上 fflush() 来强制刷新缓冲区。

例子(primes_fork.c,通过子进程来计算质数): 

#include 
#include 
#include 
#include 

int main()
{
  int max = 100;
  pid_t pid;

  for (int i = 2; i <= max; i++) {
    if ((pid = fork()) == 0) {
      // child
      int flag = 1;
      for (int j = 2; j <= i / 2; j++) {
        if (i % j == 0) {
          flag = 0;
          break;
        }
      }

      if (flag) {
        printf("%d\n", i);
      }

      exit(0);
    } else if (pid < 0) {
      perror("fork");
      exit(1);
    }
  }
  exit(0);
}

通过 man ps 可以找到进程的所有状态信息:

  • D:不可中断的睡眠态(通常是 IO);
  • I:空闲的内核线程;
  • R:运行态或可运行态;
  • S:可中断的睡眠态(等待事件的完成);
  • T:被控制信号停止;
  • X:死亡态;
  • Z:僵尸(zombie)进程,已终止但未被其父亲接收;

其中父进程如果不使用 waitpid 接收子进程状态,会导致子进程终止后变成僵尸态,会占用 pid 号,父进程终止后内核会自动将子进程交付给 init 进程,等待子进程终止后为其 “收尸”。

二、进程的消亡及释放资源(wait、waitpid)

 wait(2) 和 waitpid(2) 可以等待进程状态发生变化。

#include 
#include 

pid_t wait(int *wstatus);

pid_t waitpid(pid_t pid, int *wstatus, int options);

wait(2) 成功时返回终止的子进程的 pid,不需要指定特定的子进程 pid,并且需要死等(阻塞)。 若 wstatus 非空,则其可以一些宏函数指示进程的状态:

  • WIFEXITED(wstatus):若子进程正常终止则返回真(exit(3)、_exit(2) 或从 main 函数返回);
  • WEXITSTATUS(wstatus):返回子进程的退出状态码,前置条件是 WIFEXITED(wstatus) 必须首先为真;
  • WIFSIGNALED(wstatus):若子进程被信号终止了则返回真;
  • WTERMSIG(wstatus):检测终止子进程的信号值,前置条件是 WIFSIGNALED(wstatus) 为真;

waitpid(2) 相比于 wait(2) 可以指定等待的子进程(pid),并且可以指定一些选项(options):

  • WNOHANG:如果没有子进程退出则立即返回(非阻塞)

进程分配任务的方法:

  1. 分块(每个线程一部分任务);
  2. 交叉分配(依次给每个线程分配任务);
  3. 池(往任务池里面扔任务,线程从池中抢任务);

三、exec 函数族

 exec 函数族可以用来执行一个二进制可执行文件。

#include 

extern char **environ;

/* 需要给出文件路径 */
int execl(const char *pathname, const char *arg, ...
                       /* (char  *) NULL */);
/* 只需要文件名称,然后去环境变量environ中寻找 */
int execlp(const char *file, const char *arg, ...
                       /* (char  *) NULL */);
int execle(const char *pathname, const char *arg, ...
                       /*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);

exec 函数族会将当前进程映像替换为新的进程映像。所以在 exec 后的代码不会执行。

在 exec 之前需要 fflush(),和前面 1.1 的例子一样,写文件是全缓冲,会导致打印的内容还没写入到文件就被 exec 替换掉了进程映像。

例子,使用 fork + exec 来实现一个简单的 shell(myshell.c)

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define DELIMS " \t\n"

struct cmd_st
{
  glob_t globres;
};

static void prompt(void)
{
  printf("mysh$ ");
}

static void parse(char *line, struct cmd_st *cmd)
{
  char *tok;

  int i = 0;
  while (1) {
    tok = strsep(&line, DELIMS);
    if (tok == NULL)
      break;
    if (tok[0] == '\0') // empty str
      continue;

    glob(tok, GLOB_NOCHECK | GLOB_APPEND * i, NULL, &cmd->globres);
    i = 1;
  }
}

int main()
{
  char *linebuf = NULL;
  size_t linebuf_size = 0;
  struct cmd_st cmd;
  pid_t pid;

  while (1) {
    prompt(); // 打印提示符

    if (getline(&linebuf, &linebuf_size, stdin) < 0) {
      break;
    }

    parse(linebuf, &cmd); // 解析命令

    /* extern cmd */
    {
      pid = fork();
      if (pid < 0) {
        perror("fork");
        exit(1);
      }

      /* child process */
      if (pid == 0) {
        execvp(cmd.globres.gl_pathv[0], cmd.globres.gl_pathv);
        perror("exec");
        exit(1);
      }

      wait(NULL);
    }
  }
  exit(0);
}

可以在 /etc/passwd 文件里修改用户的登录 shell,十分有趣: 

四、守护进程

持续运行在后台,等待处理请求的进程。一次成功的登录会产生一个会话(session)

管道符:把第一个命令的标准输出作为第二个命令的标准输入(ls | more)。

Linux--setsid() 与进程组、会话、守护进程

例子(mydaemon.c)

#include 
#include 
#include 
#include 
#include 
#include 

#define FILENAME "/tmp/out"

static void daemonize(void)
{
  pid_t pid;
  int fd;;
  if ((pid = fork()) < 0) {
    perror("fork");
    exit(1);
  }

  if (pid == 0) { // child process
    if ((fd = open("/dev/null", O_RDWR)) < 0) {
      perror("open");
      exit(1);
    }
    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    if (fd > 2)
      close(fd);

    setsid();

    // change working directory
    chdir("/"); // preventing "device is busy"
    // umask(0);
    return;
  } else {
    exit(0);
    // the daemon process's parent will be the init process
  }
}

int main()
{
  FILE* fp = NULL;

  // init daemon process
  daemonize();

  // the task of daemon process
  if ((fp = fopen(FILENAME, "w")) == NULL) {
    perror("fopen");
    exit(1);
  }

  for (int i = 0; ; i++) {
    fprintf(fp, "%d\n", i);
    fflush(fp); // writting file is full buffer, so we should flush the buffer after printf()
    sleep(1);
  }

  exit(0);
}

编译运行程序后使用 ps -axj 可以看到 daemon 进程在后台运行,但是发现其 PPID(父进程 pid)不是 init 进程的 pid 1,查了一下发现是在 Ubuntu18.04 系统中,孤儿进程会被 “/lib/systemd/systemd --user” 进程领养。 

pid 为 1097 对应的进程为 /lib/systemd/systemd --user:

syslogd 服务:

  • openlog() 打开系统日志的连接;
  • syslog() 提交日志;
  • closelog() 关闭系统日志的连接;

你可能感兴趣的:(Linux系统编程,linux,运维,服务器,c语言)