太一的博客

一个程序学徒

进程

什么是程序

  • 程序是完成特定任务的一系列指令集合

什么是进程

  • 从用户的角度来看,进程是程序的一次执行过程
  • 从操作系统的核心来看,进程是操作系统分配的内存、CPU时间片等资源的基本单位
  • 每一个进程都有自己独立的地址空间与执行状态
  • UNIX 这样的多任务操作系统能够让许多程序同时运行,每一个运行着的程序就构成了一个进程

进程数据结构

  • 进程的静态描述:由三部分组成:PCB、有关程序段和该程序段对其操作的数据结构集
  • 进程控制块:用于描述进程情况及控制进程运行所需的全部信息
  • 代码段:是进程中能被进程调度程序在 CPU 上执行的程序代码段
  • 数据段:一个进程的数据段,可以是进程对应的程序加工处理的原始数据,也可以是程序执行后产生的中间或最终数据
  • PCB 是操作系统用来感知进程存在的一个重要的数据结构

进程与程序

  • 进程是动态的,程序是短暂的
  • 进程的生命周期是相对短暂的,而程序是永久的
  • 进程数据结构 PCB
  • 一个进程只能对应一个程序,一个程序可以对应多个进程

进程控制块

  • 进程描述信息
    • 进程标识符用于唯一的标识一个进程
  • 进程控制信息
    • 进程当前状态
    • 进程优先级
    • 程序开始地址
    • 各种计时信息
    • 通信信息
  • 资源信息
    • 占用内存大小及管理用数据结构指针
    • 交换区相关信息
    • I/O设备号、缓冲、设备相关的数据结构
    • 文件系统相关指针
  • 现场保护信息
    • 寄存器
    • PC
    • 程序状态字 PSW
    • 栈指针

进程标识

  • 每个进程都会分配到一个独一无二的数字编号,我们称之为“进程标识”(process identifier),或者就直接叫它 PID
  • 是一个正整数,取值范围从 232768
  • 当一个进程被启动时,它会顺序挑选下一个未使用的编号数字作为自己的 PID
  • 数字 1 一般为特殊进程 init 保留的

进程创建

  • 不同的操作系统所提供的进程创建原语的名称和格式不尽相同,但执行创建进程原语后,操作系统所做的工作却大致相同,都包括以下几点:
    • 给新创建的进程分配一个内部标识,在内核中建立进程结构
    • 复制父进程的环境
    • 为进程分配资源,包括进程映像所需要的所有元素( 程序、数据、用户栈等)
    • 复制父进程地址空间的内容到该进程地址空间中
    • 置该进程的状态为就绪,插入就绪队列

SIGCHLD

  • 当子进程退出的时候,内核会向父进程发送 SIGCHLD 信号,子进程的退出是一个异步事件(子进程可以在父进程运行的任何时刻终止)
  • 子进程退出时,内核将子进程置为僵尸状态,这个进程称为僵尸进程,它只保留最小的一些内核数据结构,以便父进程查询子进程的退出状态。
  • 父进程查询子进程的退出状态可以用 wait/waitpid 函数

wait

  • 头文件 <sys/types.h><sys/wait.h>
  • 函数功能:当我们用 fork 启动一个进程时,子进程就有了自己的生命,并将独立地运行。有时,我们需要知道某个子进程是否已经结束了,我们可以通过 wait 安排父进程在子进程结束之后。
  • 函数原型
    • pid_t wait(int *status)
  • 函数参数
    • status:该参数可以获得你等待子进程的信息
  • 返回值
    • 成功等待子进程函数返回等待子进程的 ID

waitpid

  • 函数功能:用来等待某个特定进程的结束
  • 函数原型
    • pid_t waitpid(pid_t pid, int *status, int options)
  • 参数:
    • status: 如果不是空,会把状态信息写到它指向的位置
    • options:允许改变 waitpid 的行为,最有用的一个选项是 WNOHANG,它的作用是防止 waitpid 把调用者的执行挂起
    • 返回值:如果成功返回等待子进程的 ID,失败返回 -1

wait 和 waitpid 的区别

  • 在一个子进程终止前,wait 使其调用者阻塞,而 waitpid 有一选择项,可使调用者不阻塞
  • waitpid 并不等待第一个终止的子进程 —— 它有若干个选择项,可以控制它所等待的特定进程
  • 实际上 wait 函数是 waitpid 函数的一个特例

僵尸进程

  • 当一个子进程结束运行时,它与其父进程之间的关联还会保持到父进程也正常地结束运行或者父进程调用了 wait 才告终止。
  • 进程表中代表子进程的数据项是不会立刻释放的,虽然不再活跃了,可子进程还停留在系统里,因为它的退出码还需要保存起来以备父进程中后续的 wait 调用使用。它将称为一个”僵尸进程“。

如何避免僵尸进程

  • 调用 wait 或者 waitpid 函数查询子进程退出状态,此方法父进程会被挂起。
  • 如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略 SIGCHLD 信号,该信号是子进程退出的时候向父进程发送的。

system 函数

  • 功能:system() 函数调用 /bin/sh -c command 执行特定的命令,阻塞当前进程直到 command 命令执行完毕
  • 原型:
    • int system(const char *command);
  • 返回值:
    • 如果无法启动 shell 运行命令,system 将返回 127,出现不能执行 system 调用的其他错误时返回 -1。如果 system 能够顺利执行,返回那个命令的退出码。
  • system 函数执行时,会调用 fork、execve、waitpid 等函数。

实现自己的 system 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <dirent.h>

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

#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0) \

int my_system(const char *command);

int main(int argc, char *argv[])
{
my_system("ls -l | wc -w");
return 0;
}

int my_system(const char *command)
{
pid_t pid;
int status;
if (command == NULL)
return 1;
if ((pid = fork()) < 0)
status = -1;
else if (pid == 0)
{
execl("/bin/sh", "sh", "-c", command, NULL);
exit(127);
}
else
{
while (waitpid(pid, &status, 0) < 0)
{
if (errno == EINTR)
continue;

status = -1;
break;
}
}
return status;
}

守护进程

  • 守护进程是在后台运行不受终端控制的进程,通常情况下守护进程在系统启动时自动运行
  • 守护进程的名称通常以 d 结尾,比如 sshd、xinetd、crond

创建守护进程步骤

  • 调用 fork(),创建新进程,它会是将来的守护进程
  • 在父进程中调用 exit,保证子进程不是进程组组长
  • 调用 setsid 创建新的会话期
  • 将当前目录改为根目录
  • 将标准输入、标准输出、标准错误重定向到 /dev/null