Linux 进程

Linux 进程的概念和数据结构,以及进程的接口函数的使用。

什么是程序?

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

什么是进程?

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

进程数据结构

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

进程与程序的区别

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

Linux 进程状态

  • 运行状态(TASK_RUNNING)
  • 可中断睡眠状态(TASK_INTERRUPTIBLE)
  • 不可中断睡眠状态(TASK_UNINTERRUPTIBLE)
  • 暂停状态(TASK_STOPPED)
  • 僵死状态(TASK_ZOMBIE)

进程控制块

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

进程标识符

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

进程创建

  • 给新创建的进程分配一个内部标识,在内核中建立进程结构。
  • 复制父进程的环境。
  • 为进程分配资源, 包括进程映像所需要的所有元素(程序、数据、用户栈等),
  • 复制父进程地址空间的内容到该进程地址空间中。
  • 置该进程的状态为就绪,插入就绪队列。

进程撤销

进程终止时操作系统做以下工作:

  • 关闭软中断:因为进程即将终止而不再处理任何软中断信号;
  • 回收资源:释放进程分配的所有资源,如关闭所有已打开文件,释放进程相应的数据结构等;
  • 写记帐信息:将进程在运行过程中所产生的记帐数据(其中包括进程运行时的各种统计信息)记录到一个全局记帐文件中;
  • 置该进程为僵死状态:向父进程发送子进程死的软中断信号,将终止信息status送到指定的存储单元中;
  • 转进程调度:因为此时CPU已经被释放,需要由进程调度进行CPU再分配。

复制进程镜像

  • 使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,包括:进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。
  • 子进程与父进程的区别在于:
    • 1、父进程设置的锁,子进程不继承
    • 2、各自的进程ID和父进程ID不同
    • 3、子进程的未决告警被清除;
    • 4、子进程的未决信号集设置为空集。

      SIGCHLD

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

进程系统函数

fork函数

  • 功能:创建一个子进程
  • 原型

    1
    pid_t fork();
  • 返回值:如果成功创建一个子进程,对于父进程来说返回子进程ID,对于子进程来说返回值为0,如果为-1表示创建失败

注意:

  • fork系统调用之后,父子进程将交替执行。
  • 如果父进程先退出,子进程还没退出那么子进程的父进程将变为init进程。(注:任何一个进程都必须有父进程)
  • 如果子进程先退出,父进程还没退出,那么子进程必须等到父进程捕获到了子进程的退出状态才真正结束,否则这个时候子进程就成为僵进程。

写时复制copy on write:

  • 如果多个进程要读取它们自己的那部分资源的副本,那么复制是不必要的。
  • 每个进程只要保存一个指向这个资源的指针就可以了。
  • 如果一个进程要修改自己的那份资源的“副本”,那么就会复制那份资源。这就是写时复制的含义。

fork之后父进程与子进程共享文件

父子进程同时写一个文件:

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
pid_t pid;
int fd;
if ((fd = open("test.txt", O_WRONLY)) < 0) {
ERR_EXIT("open");
}
if ((pid = fork()) == -1) {
ERR_EXIT("fork");
}
if (pid > 0) {
printf("parent process %d\n", getpid());
write(fd, "parent", 6);
sleep(3);
} else {
printf("child process %d\n", getpid());
write(fd, "child", 5);
}
return 0;
}

fork与vfork:

  • 在fork还没实现copy on write之前。Unix设计者很关心fork之后立刻执行exec所造成的地址空间浪费,所以引入了vfork系统调用。
  • vfork有个限制,子进程必须立刻执行_exit或者exec函数。
  • 即使fork实现了copy on write,效率也没有vfork高,但是我们不推荐使用vfork,因为几乎每一个vfork的实现,都或多或少存在一定的问题。

exit函数

进程终止的五种方法:

  • 正常退出
    • 从main函数返回
    • 调用exit
    • 调用_exit
  • 异常退出
    • 调用abort
    • 由信号终止


atexit函数

  • 功能:注册终止处理程序,最多可注册32个终止处理程序
  • 原型:

    1
    int atexit(void (*function)(void));
  • 返回值:成功返回0,失败返回非零

  • 注意:终止处理函数的调用与注册次序相反

exec函数组

  • 功能:把当前进程替换为一个新进程
  • 原型

    1
    2
    3
    4
    5
    6
    int execl(const char *path, const char *arg, ...);
    int execlp(const char *file, const char *arg, ...);
    int execle(const char *path, const char *arg,
    ..., char * const envp[]);
    int execv(const char *path, char *const argv[]);
    int execvp(const char *file, char *const argv[]);
  • 参数

    • path:参数表示你要启动程序的名称包括路径名
    • arg:参数表示启动程序所带的参数
  • 返回值:失败返回-1,成功不返回

注意:

  • 在进程的创建上Unix采用了一个独特的方法,它将进程创建与加载一个新进程映象分离。这样的好处是有更多的余地对两种操作进行管理。
  • 当我们创建了一个进程之后,通常将子进程替换成新的进程映象,这可以用exec系列的函数来进行。当然,exec系列的函数也可以将当前进程替换掉。
  • execl,execlp,execle(都带“l”)的参数个数是可变的,参数以一个空指针结束。
  • execv和execvp的第二个参数是一个字符串数组,新程序在启动时会把在argv数组中给定的参数传递到main
  • 这些函数通常都是用execve实现的,这是一种约定俗成的做法,并不是非这样不可。
  • 名字最后一个字母是“p”的函数会搜索PATH环境变量去查找新程序的可执行文件。如果可执行文件不在PATH定义的路径上,就必须把包括子目录在内的绝对文件名做为一个参数传递给这些函数。

实例:
hello.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
extern char** environ;
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
printf("hello pid = %d\n", getpid());
int i=0;
for (i=0; environ[i]!=NULL; ++i) {
printf("%s\n", environ[i]);
}
return 0;
}

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
pid_t pid;
if ((pid = fork()) == -1) {
ERR_EXIT("fork");
}
if (pid > 0) {
printf("parent process %d\n", getpid());
sleep(3);
} else {
char *const args[] = {"ps", NULL};
char *const envp[] = {"AA=1", "BB=2", NULL};
execve("./hello", args, envp);
printf("child process %d\n", getpid());
}
return 0;
}

wait函数

  • 功能:等待子进程退出
  • 原型

    1
    pid_t wait(int *status)
  • 参数

    • status:该参数可以获得你等待子进程的信息
  • 返回值:成功等待子进程函数返回等待子进程的ID

注意:

  • wait系统调用会使父进程暂停执行,直到它的一个子进程结束为止。
  • 返回的是子进程的PID,它通常是结束的子进程。
  • 状态信息允许父进程判定子进程的退出状态,即从子进程的main函数返回的值或子进程中exit语句的退出码。
  • 如果status不是一个空指针,状态信息将被写入它指向的位置。

退出状态:

宏定义 描述
WIFEXITED(status) 如果子进程正常结束,返回一个非零值
WEXITSTATUS(status) 如果WIFEXITED非零,返回子进程退出码
WIFSIGNALED(status) 子进程因为捕获信号而终止,返回非零值
WTERMSIG(status) 如果WIFSIGNALED非零,返回信号代码
WIFSTOPPED(status) 如果子进程被暂停,返回一个非零值
WSTOPSIG(status) 如果WIFSTOPPED非零,返回一个信号代码

实例:

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
pid_t pid;
if ((pid = fork()) < 0) {
ERR_EXIT("fork");
}
if (pid == 0) {
printf("this is child\n");
abort();
}
printf("this is parent\n");
int status;
int ret;
ret = wait(&status);
printf("ret = %d, child pid = %d\n", ret, pid);
if (WIFEXITED(status)) {
printf("child exited normal exit status=%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("child exited abnormal signal number=%d\n", WTERMSIG(status));
} else if (WIFSTOPPED(status)){
printf("child exited abnormal signal number=%d\n", WTERMSIG(status));
}
return 0;
}


waitpid函数

  • 功能:用来等待某个特定进程的结束
  • 原型:

    1
    pid_t waitpid(pid_t pid, int *status,int options)
  • 参数:

    • status:如果不是空,会把状态信息写到它指向的位置
    • options:允许改变waitpid的行为,最有用的一个选项是WNOHANG,它的作用是防止waitpid把调用者的执行挂起
  • 返回值:如果成功返回等待子进程的ID,失败返回-1

pid参数:
对于waitpid的pid参数的解释与其值有关:

  • pid == -1 等待任一子进程。于是在这一功能方面waitpid与wait等效。
  • pid > 0 等待其进程ID与pid相等的子进程。
  • pid == 0 等待其组ID等于调用进程的组ID的任一子进程。换句话说是与调用者进程同在一个组的进程。
  • pid < -1 等待其组ID等于pid的绝对值的任一子进程。

system函数

  • 功能:system()函数调用“/bin/sh -c command”执行特定的命令,阻塞当前进程直到command命令执行完毕
  • 原型:

    1
    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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
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);
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
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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
//set_demon(0, 0);
daemon(0, 0);
for(;;);
return 0;
}
int set_demon(int nochdir,int noclose) {
pid_t pid;
if ((pid = fork()) < 0) {
ERR_EXIT("fork");
}
if (pid > 0) {
exit(0);
}
setsid();
if (nochdir == 0) {
chdir("/");
}
if (noclose == 0) {
int i;
for (i=0; i<3; ++i) {
close(i);
}
open("/dev/null", O_RDWR);
dup(0);
dup(0);
}
return 0;
}

daemon函数

  • 功能:创建一个守护进程
  • 原型:

    1
    int daemon(int nochdir, int noclose);
  • 参数:

    • nochdir:=0将当前目录更改至“/”
    • noclose:=0将标准输入、标准输出、标准错误重定向至“/dev/null”
  • 返回值:成功返回0,失败返回-1