概述
Linux 是多任务操作系统,可以同时运行多个进程,来完成多项工作。
进程就是处于活动状态的程序,占用一定的内存空间。进程可以把自己复制一份,从而创造出一个新的进程。新的进程称为 子进程,原来的进程称为 父进程
进程可以复制自己。这意味着启动一个程序,可能会产生多个进程。这样的程序能同时进行多项工作。多进程编程就是要设计一个这样的程序。
进程的调度
实际上 CPU 只能同时处理一个进程的工作。也就是说,进程并不是真的同时都在运行。
Linux 是一个 分时 操作系统。在同一时刻,只有一个进程得到 CPU 的处理,但很快就会变成另一个进程,如此往复。虽然每个进程一次只占用 CPU 很短的一段时间,但是每个进程总是很快就能再一次占用 CPU,所以这些进程看起来就好像一直都在运行一样。
CPU 在工作时就好像人体的循环系统一样。心脏的跳动维持血液的流动;晶振的振荡维持 CPU 的运行。心脏一旦停止跳动,血液也就停止流动;晶振一旦停止振荡,CPU 也就停止工作。
晶振在单位时间内的振荡次数称为 时钟频率,两次振荡之间的时间间隔称为 时钟周期。进程占用 CPU 的时间应该以时钟周期为单位来计量。
进程的状态
进程从创建到运行结束,经历的全部过程,称为进程的生命周期。在生命周期的不同阶段,进程会呈现不同的状态。下表列出了进程可能出现的所有状态。
状态 | 含义 |
---|---|
创建状态 | 正在被创建 |
就绪 | 刚刚创建好,还没运行过 |
内核状态 | 运行中 |
用户状态 | 暂停中 |
睡眠 | 已经轮到这个进程上场了,但是它的某些需求得不到满足,只能继续等待 |
唤醒 | 正在睡眠的进程,正在被唤醒 |
被抢占 | 运行到一半,CPU 被另一个进程抢占 |
僵死状态 | 进程已经结束,但记录还在 |
进程的控制
使用下文讨论的几个函数需要包含下面几个头文件。
#include <stdlib.h> // exit()
#include <unistd.h> // fork(), sleep(), _exit(), pid_t
#include <sys/wait.h> // wait()
fork() 创建子进程
子进程通过 fork()
函数创建。fork()
不需要任何参数,返回值是 pid_t
型。pid_t
型实际上就是 int
型。这是专门用来保存进程 PID(进程的编号)的类型。
pid_t fork(void);
如果子进程创建成功,fork()
函数将返回子进程的 PID,否则返回 -1
前面说过,创建子进程相当于把自己复制一份。也就是说,创建出来的子进程和父进程几乎是一模一样的,并且都将接着执行 fork()
函数后面的代码。
不同的是,对于 fork()
函数的返回值,在子进程中将得到 0
。因此,如果在 fork()
函数之后用一个 if
语句对 fork()
函数的返回值进行判断,子进程和父进程将进入不同的分支。
pid_t cpid;
cpid = fork();
if (cpid == -1) {
printf("Create process failed!\n");
exit(1);
}
if (cpid == 0) {
printf("Hello from Child!\n");
} else {
printf("Hello from Parent!\n");
}
sleep() 主动睡眠
进程调用 sleep()
函数,将进入睡眠状态,传递给 sleep()
函数的参数就是睡眠的持续时间,单位秒。下面代码将使进程进入睡眠状态,持续 3 秒钟。
sleep(3);
wait() 等待进程结束、exit() 进程结束
父进程调用 wait()
,将进入睡眠状态,以等待子进程进入僵死状态。子进程调用 exit()
将使自己进入僵死状态。
pid_t wait(int * status);
void exit(int status);
这两个函数都只有一个参数。exit()
函数的参数是一个整型变量,用来保存一个范围在 0-255
之间的整数。wait()
函数的参数要求一个整型变量的地址,这个整型变量将保存这个整数。
多线程
进程进一步细分,就是线程。每一个进程都至少有一个线程,这个线程称为 主线程。主线程就是运行主函数 main()
的线程。创建线程相当于调用一个函数,只不过原来的线程会立即执行后续的代码而不等待这个函数返回。这使得被调函数中的代码和后续的代码是并行执行的。因此,可以简单地认为多线程就是同时运行多个函数。
历史上曾出现过多种线程标准。这些标准互不兼容,这使得程序员难以开发可移植的应用程序。为此,IEEE 制订了后来被广泛采用的线程标准 POSIX threads,简称 Pthreads。POSIX 线程库 实现了这个标准。POSIX 线程库也是最常用的线程库。使用 POSIX 线程库需要包含头文件 pthread.h
#include <pthread.h>
由于 POSIX 线程库并不属于默认库,因此在使用
gcc
命令进行编译时,要加上-lpthread
选项。
pthread_create() 创建线程
线程通过调用 pthread_create()
函数创建。
int pthread_create(
pthread_t * id,
pthread_attr_t * attr,
void * (* start_routine)(void *),
void * arg
);
-
第一个参数要求一个
pthread_t
变量的地址。这个变量用来保存线程的标识符 -
第二个参数要求一个
pthread_attr_t
结构的地址。这个结构用于设定线程的一些属性,一般设为0
-
第三个参数要求一个函数。创建的线程将调用这个函数。这个函数称为 线程函数。线程函数必须以一个
void
指针为参数,返回值也必须是一个void
指针。 -
第四个参数是一个
void
指针,它将会作为线程函数的参数。如果不需要传参,设为0
如果线程创建成功,pthread_create()
函数将返回 0
,否则返回要给错误代码。这些错误代码是线程库定义的一些常量,但没有一个是 -1
pthread_exit() 线程结束
线程调用 pthread_exit()
函数可结束自己,这个函数相当于结束进程的 exit()
void pthread_exit(void * retval);
唯一的参数是一个 void
指针,用来指向返回值。
pthread_join() 等待线程结束
可以调用 pthread_join()
函数来等待另一个线程结束。
int pthread_join(pthread_t id, void ** retval);
-
第一个参数要求一个线程的标识符
-
第二个参数要求一个
void
指针的地址。这个指针将被指向线程的返回值。如果不需要得到线程的返回值,可设为0
如果顺利,pthread_join()
函数将返回 0
,否则返回一个错误代码。
下面是一个完整的例子。演示了从创建线程到结束线程的过程。
#include <stdio.h>
#include <stdlib.h> // exit()
#include <unistd.h> // fork(), sleep(), _exit(), pid_t
#include <pthread.h>
void * hello(void * arg)
{
printf("Thread start running!\n");
printf("%s\n", (char *)arg);
sleep(3);
pthread_exit("Hello from thread!");
}
int main(void)
{
pthread_t id;
void * thread_retval;
if (pthread_create(&id, 0, hello, "Hello from main!") != 0) {
printf("Create thread failed!\n");
exit(1);
}
pthread_join(id, &thread_retval);
printf("%s\n", (char *)thread_retval);
return 0;
}
pthread_detach() 脱离同步
pthread_detach()
函数用来使一个线程与其他线程脱离同步。脱离同步是指其他线程不能用 pthread_join()
函数来等待这个线程结束。这个线程将在退出时自行释放所占的资源。
int pthread_detach(pthread_t id);
pthread_detach()
函数唯一的参数就是需要脱离同步的线程的标识符。如果顺利,将返回 0
,否则返回一个错误代码。
网友评论