explorer

万丈高楼平地起,勿在浮沙筑高台

0%

[What]Linux 中多进程和多线程编程

对多进程编程的知识总是零零散散,正好再来复习一下。

进程

fork 系统调用

1
2
3
4
#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);

调用 fork() 函数后,系统便会创建子进程,只是当前内存的复制是写时复制,这些概念耳熟能详。

但也有需要注意的:

  • 子进程的信号位图被清除,也就是原进程设置的信号处理函数不再对新进程起作用
  • 父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符引用计数加 1,而且父进程的用户根目录、当前工作目录等变量的引用计数都会加 1

exec 系统调用

执行 pathfile 指定的可执行文件, arg 指定可变参数, argv[] 指定参数数组, envp 可设置环境变量。

  • 因为此时原程序已经被新的可执行文件完全替换

exec 类调用后,除非新的可执行文件出错,否则是不会返回的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>

extern char **environ;

int execl(const char *path, const char *arg, ...
/* (char *) NULL */)
;

int execlp(const char *file, const char *arg, ...
/* (char *) NULL */)
;

int execle(const char *path, const char *arg, ...
/*, (char *) NULL, char * const envp[] */)
;

int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[])
;

处理僵尸进程

如之前所说, 子进程退出后内核会保存该进程的进程表项,用于被父进程获取该信息,在子进程退出后父进程获取信息前,该进程处于僵尸态。

1
2
3
4
5
6
7
#include <sys/types.h>
#include <sys/wait.h>

//阻塞的等待,直到该进程的某个子进程结束运行,子进程的退出状态存储于 wstatus 中
pid_t wait(int *wstatus);
//指定等待某个子进程结束运行
pid_t waitpid(pid_t pid, int *wstatus, int options);

为了能很好的解释子进程的退出状态, sys/wait.h 提供了如下宏:

含义
WIFEXITED(wstatus) 如果子进程正常结束,返回真
WEXITSTATUS(wstatus) 在 WIFEXITED 返回真后,使用此宏得到子进程的退出码
WIFSIGNALED(wstatus) 如果子进程被信号终止,返回真
WTERMSIG(wstatus) 当 WIFSIGNALED 返回真后,此宏返回对应信号值
WIFSTOPPED(wstatus) 如果子进程被信号停止,返回真
WSTOPSIG(wstatus) 当 WIFSTOPPED 返回真后,此宏返回对应信号值

为了提高程序运行效率,通常的做法是:父进程接收 SIGCHLD 信号,当子进程结束后便会发送此信号,父进程再在信号处理函数中进行 waitpid 获取子进程退出状态。

管道

管道的应用在之前已经总结过了。

只是还需要强调一下: 父子进程间传递数据,利用的是 fork 调用之后两个管道文件描述符都保持打开。 但由于管道是单向数据流通信,所以父进程和子进程必须有一个关闭 fd[0],另一个关闭 fd[1]。 所以要实现父子进程之间的双向数据传输,就得使用两个管道。

信号量、共享内存、消息队列

这里已经熟悉过信号量了。

这里已经熟悉过共享内存了。

这里已经熟悉过消息队列了。

线程

线程模型

线程分为内核线程和用户线程:

  • 内核线程运行在内核空间,由内核调度
  • 用户线程运行在用户空间,由线程库调度。

当进程的一个内核线程获得 CPU 使用权时,就加载并运行一个用户线程。一个进程可以拥有 M 个内核线程和 N 个用户线程,并且 M <= N。

根据 M 和 N 的比值来看,线程的实现方式分为三种模式:完全在用户空间实现、完全由内核调度和双层调度。

完全在用户空间实现:当一个进程运行时,内核空间会对应一个内核线程以调度该进程对应的初始线程。而后此进程在用户空间创建的多个线程对内核来说是不可见的,这些线程由线程库实现调度。 这种方式创建的线程无内核干预,所以创建和调度的速度相对快,并且不占用内核调度资源。但是其缺点在于无法利用多核实现并行。

  • 虽然这些线程可以创建不同的优先级,但是对外来说只有一个优先级。

完全由内核调度:每当用户空间创建一个线程,就会在内核对应一个线程,这与 完全在用户空间实现 的优缺点正好相反。

双层调度:结合前面两种实现方式,M 个内核线程对应 N 个用户态线程,结合了二者的优点。

Linux 线程库

Linux 中默认的 NPTL 库使用的是一个内核线程对应一个用户线程的模式。

Last Updated 2020-06-28 Sun 11:59.
Render by hexo-renderer-org with Emacs 26.3 (Org mode 9.3.7)