进程是执行的程序。
进程不止是程序代码,程序代码有时称为文本段(text section)(或代码段 (code section))。进程还包括当前活动,如程序计数器(Program Counter, PC) 的值和处理器寄存器的内容等。另外进程通常还包括:进程堆栈(stack)(包括临时数据,如函数参数、返回地址和局部变量)和数据段(data section)(包括全局变量)。进程还可能包括堆(heap),这是进程运行时动态分配的内存。
随着进程的运行,进程的状态可能会发生改变。进程的状态可能包括:创建、运行、就绪、等待、终止。
在操作系统内,进程可以用它的**进程控制块(PCB)**来表示。每个进程控制块包括:进程状态、程序计数器(指定进程要执行的下条指令地址)、CPU 寄存器、CPU 调度信息、内存管理信息、统计信息、I/O 状态信息。
进程在其执行过程中能通过创建进程系统调用(create-process system call)创建多个新进程。创建进程称为 父(parent)进程,被创建的新进程称为 子(children)进程 。每个新进程可以再创建其他进程以形成 进程树(tree) 。
多数操作系统根据一个唯一的 进程标识符(process identifier,pid) 来识别进程,通常是整数值。
一个进程创建子进程时,子进程可能通过以下方式获取资源:
父进程与子进程存在两种执行关系:
新进程的地址空间有两种可能
在 UNIX 操作系统中,通过 fork() 系统调用来创建新进程,新进程的地址空间复制了原来进程的地址空间。这种机制允许父进程与子进程轻松通信。通常,在系统调用 fork() 后,有个进程使用系统调用 exec(),以用新程序来取代进程的内存空间并开始执行。这种方式使得两个进程能相互通信,且按各自方式运行。如果父进程在子进程运行时没什么可做,那么它可以采用系统调用 wait() 把自己移出就绪队列,直到子进程终止。
exit()
时进程终止,此时进程可以返回状态值(通常为整数)给父进程(通过 wait()
获取),所有进程资源也会被操作系统释放。操作系统内并发进行的进程可以为独立进程或协作进程。如果一个进程不能影响其他进程,也不能被其他进程所影响,则改进程时独立的,否则改进程是协作的。
进程协作需要进程间通信机制来允许进程相互交换数据,其包括两种基本模式:
线程是进程内的控制流。线程是 CPU 使用的基本单元,由线程 ID、程序计数器、寄存器集合和堆栈组成。它与同一进程的其他线程共享代码段、数据段和其他操作系统资源,如打开文件和信号。
多线程的优点包括:用户响应的改进、进程内资源的共享、经济和可扩展性的因素(如更有效地使用多个处理核)。
有两种方法提供线程支持:用户层的用户线程(user thread)和内核层的内核线程(kernal thread)。用户线程位于内核之上,它的管理无需内核支持;而内核线程由操作系统来直接支持与管理。几乎所有的现代操作系统都支持内核线程。用户线程对程序员是可见的,而对内核是未知的。通常。用户线程与内核线程相比,创建和管理要更快,因为它并不需要内核干预。
三种不同类型模型关联用户线程和内核线程。多对一模型将多个用户线程映射到一个内核线程;一对一模型将每个用户线程映射到一个对应内核线程;多对多模型将多个用户线程在同样(或更少数量)的内核线程之间切换。
线程库为程序员提供创建和管理线程的 API。实现线程库的主要方法有两种:
常用的主要线程库有三个: POSIX Pthreads、Windows 线程和 Java 线程。
#include
#include
int sum = 0; /* this data is shared by the thread(s) */
void *runner(void *param); /* thread call this thread */
int main(int argc, char *argv[]){
pthread_t tid; /* the thread identifier */
pthread_attr_t attr; /* set of thread attributes */
if (argc != 2){
return -1;
}
if (atoi(argv[1]) < 0){
return -1;
}
/* get the default attributes */
pthread_attr_init(&attr);
// create the thread
pthread_create(&tid, &attr, runner, argv[1]);
// wait for the thread to exit
pthread_join(tid, NULL);
printf("sum = %d\n", sum);
}
/* The Thread will begin control in this function */
void *runner(void *param){
int i, upper = atoi(param);
for (i = 1; i <= upper; i++)
sum += i;
pthread_exit(0);
}
#include
#include
DWORD sum = 0; /* this data is shared by the thread(s) */
/* The thread runs in this separate function */
DWORD WINAPI Summation(LPVOID Param){
DWORD Upper = *(DWORD*)Param;
for (DWORD i = 0; i <= Upper; i++){
Sum += i;
}
return 0;
}
int main(int argc, char *argv[]){
DWORD ThreadId;
HANDLE ThreadHandle;
int Param;
if (argc != 2){
return -1;
}
if ((Param = atoi(argv[1])) < 0){
return -1;
}
/* Create the thread */
ThreadHandle = CreateHandle(
NULL, // default security attributes
0, // default stack size
Summation, // thread function
&Param, // parameter to thread function
0, // default creation flags
&ThreadId); // returns the thread identifier
if (ThreadHandle != NULL){
/* wait for the thread to finish */
WaitForSingleObject(ThreadHandle, INFINITE);
/* Close the thread handle */
CloseHandle(ThreadHandle);
printf("sum = %d\n", Sum);
}
}
除了采用线程库 API 来显式创建线程,也可以使用隐式线程,这种线程的创建和管理交由编译器和运行时库来完成。隐式线程方法包括:线程池、OpenMP 和 Grand Central Dispath 等。
如果允许所有的并发请求都通过新线程创建来处理,则没法限制系统内的并发执行线程的数量。无限制的线程可能耗尽系统资源,如 CPU 时间和内存。解决这一问题的一种方法是使用线程池(Thread Pool)。
线程池的主要思想是:在进程开始时创建一定数量的线程,并加到池中以等待工作。当服务器收到请求时,它会唤醒池内的一个线程(如果有可用线程),并将服务的请求传递给它。一旦线程完成服务,它会返回到池中再等待工作。如果池内没有可用线程,那么服务器会等待,直到有空线程为止。
线程池具有以下优点:
多线程程序为程序员带来了许多挑战,包括 fork() 和 exec() 系统调用的语义。其他问题包括信号处理、线程撤消、线程本地存储(Thread-Local Storage, TLS)和调度激活等。
TLS 与局部变量容易混淆。局部变量只有再单个函数调用时才可见;而 TLS 在多个函数调用时都可见。在某些方面,TLS 类似静态 (Static) 数据,不同的是,TLS 数据是每个线程特有的。