Linux 多进程、多线程编程
Linux 是多任务操作系统,可以同时运行多个进程,来完成多项工作。
进程就是处于活动状态的程序,占用一定的内存空间。进程可以新建进程,新的进程称为 子进程,原来的进程称为 父进程。
进程可以复制自己。这意味着启动一个程序,可能会产生多个进程。这样的程序能同时进行多项工作。多进程编程就是要设计一个这样的程序。
进程的状态
进程从创建到运行结束,经历的全部过程,称为进程的生命周期。在生命周期的不同阶段,进程会呈现不同的状态。下表列出了进程可能出现的所有状态。
状态 | 含义 |
---|---|
创建状态 | 正在创建 |
就绪 | 刚刚创建好,还没运行过 |
内核状态 | 运行中 |
用户状态 | 暂停中 |
睡眠 | 运行中的进程因为某些需求得不到满足而进入等待状态 |
唤醒 | 睡眠中的进程,正在被唤醒 |
被抢占 | 运行期间,CPU 被另一个进程抢占 |
僵死状态 | 进程已经结束,但内存空间等占用的资源还未释放,被称为僵尸进程 |
进程的管理
每个进程都有一个唯一的编号,记为 PID (process id)。PID 应该用 pid_t
类型的变量来保存,pid_t
类型其实是 unsigned int
的别名。
下面两个函数一个返回当前进程的 PID,一个返回父进程的 PID
1 |
|
在 Linux 系统中,除了 init
进程,其他进程都有一个父进程。对于在终端启动程序,它的父进程就是 Shell。下面程序在运行时输出自己的 PID 和父进程的 PID
1 |
|
fork() 复制进程
fork()
函数以父进程为蓝本,产生一个子进程。
1 |
|
对于 fork()
函数的返回值,如果创建失败,将得到 -1;如果创建成功,在父进程中将得到子进程的 PID,在子进程中将得到 0
如果在 fork()
函数之后用一个 if
语句对 fork()
函数的返回值进行判断,子进程和父进程将进入不同的分支。
下面程序中的 if
语句让父进程输出 parent hello!
,而子进程输出 child hello!
1 |
|
sleep() 进入睡眠
调用 sleep()
函数,进程将进入睡眠状态,传递给 sleep()
函数的参数就是睡眠的持续时间,单位秒。下面代码将使进程进入睡眠状态,持续 3 秒钟。
1 |
|
wait() 等待进程结束、exit() 结束进程
父进程调用 wait()
,将进入睡眠状态,直至子进程进入僵死状态,返回值是子进程的 PID。子进程调用 exit()
将使自己进入僵死状态。
1 |
|
这两个函数都只有一个参数。exit()
函数的参数是一个整型变量,用来保存一个范围在 0-255
之间的整数,最终这个整数的地址会保存在 wait()
函数的参数中。
进程间通信
信号机制是进程间的一种通信机制。信号是一系列整数,用来代表不同的事情发生。信号可以被进程捕获,捕获时会调用提前指定的信号处理函数,信号会作为参数传递。信号可以是由另一进程产生,也可由内核发出。
实际上,子进程调用 exit()
时就会产生一个名为 SIGCHLD
的信号,父进程只要提前为这个信号指定处理函数就好,而不用专门调用 wait()
来等待。
signal() 绑定信号处理函数
调用 signal()
函数可以为某个信号指定处理函数。
1 |
|
signal()
函数只接受两个参数,一个用于指定信号,一个用于给出信号的处理函数。信号处理函数必须是无返回值、只接受一个整数作为参数的函数。
如果成功,signal()
函数的返回值是原本的信号处理函数,否则返回 SIG_ERR
信号 SIGINT
是一个在前台运行的进程在用户按下 Ctrl + C 时可以捕获到的信号。下面程序将信号 SIGINT
的处理函数设为 sig_handler()
,这个函数会输出 interrupted!
,故用户按下 Ctrl + C 时会提示 interrupted!
1 |
|
信号处理函数除了可以是自定义的函数,还可以使用 SIG_IGN
和 SIG_DFL
这两个宏。
1 |
SIG_IGN
表示忽略目标信号;SIG_DFL
表示采用信号的默认处理方式。
处理僵尸进程
子进程终止时,父进程可以捕获 SIGCHLD
信号。如果父进程不对 SIGCHLD
信号进行处理,子进程会继续占用资源而成为僵尸进程。
最简单的解决办法是把 SIGCHLD
信号的处理函数指定为 SIG_IGN
1 | signal(SIGCHLD, SIG_IGN); |
kill()、raise() 发送信号
进程可以用 signal()
函数给信号绑定信号处理函数,一旦信号到来,信号处理函数就会被调用。进程还可以用 kill()
函数给另一进程发送信号,或者用 raise()
函数给自己发送信号。
1 |
|
kill()
函数在成功时返回 0,失败时返回 -1,errno
的可能取值如下:
EINVAL
无效的信号EPERM
无权限发送信号ESRCH
目标不存在
下面程序创建的子进程在给信号 SIGINT
绑定处理函数后进入死循环,父进程在睡眠 3 秒后向子进程发送信号 SIGINT
,使得子进程中的信号处理函数被调用,最终输出 interrupted!
1 |
|
多线程
进程进一步细分,就是线程。每一个进程都至少有一个线程,这个线程称为 主线程。主线程就是运行主函数 main()
的线程。创建线程相当于调用一个函数,只不过原来的线程会立即执行后续的代码而不等待这个函数返回。这使得被调函数中的代码和后续的代码是并行执行的。因此,可以简单地认为多线程就是同时运行多个函数。
历史上曾出现过多种线程标准。这些标准互不兼容,这使得程序员难以开发可移植的应用程序。为此,IEEE 制订了后来被广泛采用的线程标准 POSIX threads,简称 Pthreads。POSIX 线程库 实现了这个标准。POSIX 线程库也是最常用的线程库。使用 POSIX 线程库需要包含头文件 pthread.h
1 |
由于 POSIX 线程库并不属于默认库,因此在使用
gcc
命令进行编译时,要加上-lpthread
选项。
pthread_create() 创建线程
线程通过调用 pthread_create()
函数创建。
1 | int pthread_create( |
- 第一个参数要求一个
pthread_t
变量的地址。这个变量用来保存线程的标识符 - 第二个参数要求一个
pthread_attr_t
结构的地址。这个结构用于设定线程的一些属性,一般设为0
- 第三个参数要求一个函数。创建的线程将调用这个函数。这个函数称为 线程函数。线程函数必须以一个
void
指针为参数,返回值也必须是一个void
指针。 - 第四个参数是一个
void
指针,它将会作为线程函数的参数。如果不需要传参,设为0
如果线程创建成功,pthread_create()
函数将返回 0
,否则返回要给错误代码。这些错误代码是线程库定义的一些常量,但没有一个是 -1
pthread_exit() 结束线程
线程调用 pthread_exit()
函数可结束自己,这个函数相当于结束进程的 exit()
1 | void pthread_exit(void * retval); |
唯一的参数是一个 void
指针,用来指向返回值。
pthread_join() 等待线程结束
可以调用 pthread_join()
函数来等待另一个线程结束。
1 | int pthread_join(pthread_t id, void ** retval); |
- 第一个参数要求一个线程的标识符
- 第二个参数要求一个
void
指针的地址。这个指针将被指向线程的返回值。如果不需要得到线程的返回值,可设为0
如果顺利,pthread_join()
函数将返回 0
,否则返回一个错误代码。
下面是一个完整的例子。演示了从创建线程到结束线程的过程。
1 |
|
pthread_detach() 脱离同步
pthread_detach()
函数用来使一个线程与其他线程脱离同步。脱离同步是指其他线程不能用 pthread_join()
函数来等待这个线程结束。这个线程将在退出时自行释放所占的资源。
1 | int pthread_detach(pthread_t id); |
pthread_detach()
函数唯一的参数就是需要脱离同步的线程的标识符。如果顺利,将返回 0
,否则返回一个错误代码。