Linux进程与线程总结 [推荐]

清泛原创

本文介绍了Linux环境下进程与线程的基本概念以及它们之间的差异,简要介绍了Linux进程与多线程编程的基本方法及同步机制,还介绍了进程间通信IPC(InterProcess Communication)的几种方式。并通过一定的实例介绍相关技术在实际中的运用情况,使读者能够对Linux环境下进程与线程编程在整体上有一定的把握。

关键字:Linux、进程、多进程、线程、多线程、同步控制、IPC通信

1.Linux进程与线程:

本章主要介绍Linux进程、线程以及Linux进程与线程的相关比较,使读者对Linux下的运行环境有大致的认识。

1.1. Linux进程及进程同步

进程是操作系统中执行特定任务的一个实体,在保护模式下每个进程拥有其特定的指令空间及内存空间,Linux环境下每一个程序可以对应一个或多个进程,可以由一个主进程管理多个子进程,这就是Linux特有的多进程模式,而Windows无直接的多进程模式且一般使用多线程编程而不是多进程。

Linux下由fork函数创建的新进程被称为子进程(child process)。在fork调用之后是父进程还是子进程先执行是不确定的,这取决于系统内核及系统当前的运行状态等,也就是说此时发生了竞争条件(race condition),这就要求某种方式用以实现父、子进程之间的相互同步。

如果一个进程等待一个子进程终止,则它必须调用wait()函数,若子进程尚未终止,则父进程将发生阻塞;如果子进程等待其父进程终止,则可以使用下面的循环方式:

while(getppid() != 1)
     sleep(1);

这种循环称为轮询(polling),由于所有的进程都共有一个最原始的父进程init,其pid为1,所以每隔1秒查询一次父进程状态,直至父进程终止。

以上由fork函数创建的进程是具有亲缘关系的进程,即父子进程或兄弟进程。其原理实际上与传统的没有亲缘关系的独立进程类似,切勿将其与多线程概念混淆,因为每个进程都拥有其独立的内存空间。


1.2.Linux线程

实际上线程是由进程演化而来的,一个标准的进程可以看成只有一个控制线程,线程才是真正的执行单元,真正消耗CPU的时间片,所以该进程在同一时刻只做一件事情。但是实际往往需要在单进程环境中执行多个任务,这时就要用到多线程模式,每个线程处理各自独立的任务,这样可以改善程序的效率。一个进程中的所有线程都可以访问该进程的所有资源,因此对于多线程同步控制较为容易,而独立的多进程模式则使用操作系统提供的较为复杂的机制才能实现同步和通信。实际应用中由于需要往往对两者进行结合使用,以下是多进程、多线程结合使用的一个例子:

守护进程,也就是通常所说的Daemon进程,是Linux中的后台服务进程,通常伴随着操作系统的启动而运行,关闭而终止。守护进程一般来说生存期较长,独立于控制终端并周期性地执行某项任务或等待某些事件的发生,不在任何终端上显示信息同时也不会被任何终端的信息所打断。

不难看出,上例子中根据不同的业务功能划分为独立的模块均作为独立的进程启动,模块内业务由于有并发处理的需求采用多线程编程,而各模块之间的信息交流则采用IPC通信的方式,后续会进行详细的介绍。


1.3.Linux进程与线程的比较
进程和线程都是任务并发处理的途径,那么它们之间到底有何区别呢,下面从几个方面分别进行比较:
  进程 线程
内存空间 子进程是父进程的副本,子进程获得父进程的数据空间、堆和栈的副本;父、子进程的内存空间相互独立。 多个线程可以访问相同的内存空间。
功能 负责系统资源的管理 负责执行最终的任务
单位 资源管理的最小单位 程序执行的最小单位
上下文切换开销 上下文切换开销较大 上下文切换开销较小

2.Linux多线程及线程同步:
2.1.Linux多线程的使用
以下是Linux线程的创建、等待及销毁的函数:
#include <pthread.h>
int pthread_create(pthread_t *tidp, const pthread_attr_t *attr, void *func(void), void *arg);
int pthread_join(pthread_t thread, void **rval_ptr);
void pthread_exit(void *rval_ptr);

创建线程时,第一个参数为指向线程标识符结构pthread_t的指针,第二个参数为线程属性设置结构的指针,一般可以设为NULL使用默认属性,最后两个参数是线程创建后立刻执行的回调函数的函数指针及回调函数的参数。
线程创建成功后,开始执行回调函数,此时调用pthread_join等待线程的终止,第一个参数为线程标识符结构,第二个参数为指向用户定义指针的指针,线程正常终止情况下返回回调函数的返回值,若回调函数中调用pthread_exit强制退出线程,则返回pthread_exit函数中参数的指针。因此,线程退出不仅仅可以返回一个int型数值,还可以返回一个复杂的数据结构。


2.2.Linux多线程的同步机制
2.2.1 互斥量(Mutex)

互斥量从本质上说就是一把锁,确保同一时间只有一个线程访问资源。对互斥量进行加锁后,任何其他试图再次对该互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程处于阻塞状态,则所有在该互斥锁上的阻塞线程都会变为可运行状态。

如果线程不希望被阻塞,不希望等待其他线程释放互斥锁而是以一种尝试的方式试图访问共享资源,此时可以用pthread_mutex_trylock函数,若此时其他线程正在访问该资源则直接出错返回,否则加锁成功。
如果同一线程试图对同一互斥量进行重复加锁,那么它自身就会陷入死锁状态,即第二次加锁后线程发生阻塞,等待第一次释放互斥锁,而该线程已经处于阻塞状态无法释放第一被加锁的互斥量,这就直接导致死锁。Linux还有一种递归锁(RecursiveMutex),用PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP宏初始化pthread_mutex_t结构即可,同一个线程可以多次获取同一个递归锁,不会产生死锁。

2.2.2 读写锁(Rdlock)

读写锁允许多个线程同时对共享资源进行读操作,但不允许同时进行写操作。从广义上讲,可以认为它是一种共享的互斥锁,如果对于某共享资源大部分是读操作而只有少量的写操作,读写锁在一定程度上能够降低线程互斥产生的代价。

2. 2.3 信号量(Semaphore)

信号量一般用于对有限的资源进行调配管理,即一个信号量最多可以被锁住n次,其中n是信号量产生时指定的,当某线程试图对该信号量进行第n+1次加锁,则该线程将会发生阻塞,直到前n次加锁中任意一次释放该信号量。信号量可以看作是容量为1的特殊的互斥量。另外,信号量还可以用于进程间的同步。

信号量的一个比较常见的用途是限制队列的长度,在并发编程中,通常需要线程池与工作队列相结合的方式实现,这样就能合理地分配利用有限的CPU与内存资源,以至于不会出现大量操作在内存中等待运行或大量操作并发运行的尴尬局面。以下是线程池与信号量结合使用的一个实例:

指定线程池的最大线程数n,然后创建一个信号量,用n初始化该信号量。接收到操作请求时,以异步的操作方式将其放入操作队列,主线程在遍历操作队列时,通过信号量来控制并发运行的线程数,当并发运行的线程数达到最大值,由于此时信号量已满,因此后续操作将会发生阻塞,直至前面操作运行完毕释放信号量。

2. 2.4 条件变量(Cond)

条件变量类似于一个触发器,通常与互斥量结合使用,一个线程等待一个条件变量被触发,否则便会发送阻塞;条件变量阻塞后会同时对互斥对象进行解锁。可以理解为条件变量具有通知功能,符合一定条件后,通知其他等待该条件变量的线程可以继续访问该共享资源,当然通知的方式可以是单一线程的通知或广播通知所有其他的线程。


3.Linux IPC通信:

让拥有依赖关系的进程协调工作,这样才能达到进程的共同目标。可以使用两种技术来达到协调。第一种技术在具有通信依赖关系的两个进程间传递信息,这种技术称作进程间通信(IPC)。第二种技术是同步,当进程间相互具有合作依赖时使用。这两种类型的依赖关系可以同时存在,从理论上讲,同步是通信的一个子集,它是一种不传递实际数据的通信,也就是告诉对方现在还不能访问某资源或通知对方现在可以访问了。从实现方式来讲,同步和通信的确有一定的差异,用于进程间的信号量就是用来同步的,而不能进行数据通信;共享内存区既是一种通信手段,但同时也需要同步与互斥,也就是要保护共享内存区。

3.1 管道(PIPE)

通俗地讲,Linux管道类似于生活中的自来水管道,水源进入水管流向水龙头,若此时关闭水龙头,该水管中水便会停止流动,此时水源也无法继续流入水管;若水源被切断,该水管中剩余的水仍然能够从水龙头流出。类似地,Linux管道也是如此,如果没有进程读取管道一端的数据,则写入管道的数据填满管道空间后将发生阻塞,而且管道的缓冲区是有限的;管道写入端关闭后,写入的数据将一直存在,直到被读出为止。Linux管道是半双工的,数据只能向一个方向流动,当需要双方通信时,需要建立起两个管道,而且它没有名字,只能用于具有亲缘关系(父子进程或兄弟进程)的进程之间,原因很简单,因为定义的管道在操作系统范围内没有标识符,而只是临时定义两个整型变量作为管道的两端,该局部变量不能用于两个独立的进程之间。

下面是管道的创建函数:

#include <unistd.h>
int pipe(int fd[2])

函数参数为一个长度为2的整型数组,fd[0]表示管道的读端,fd[1]表示管道的写端,如果读写端顺序颠倒,将导致错误发生。另外管道的读写及关闭操作均利用一般文件I/O的操作函数read、write、close。

3.2 命名管道(FIFO)

管道很重要的限制是它没有名字,不能用于独立的进程间的通信。而命名管道则克服了这一限制,命名管道提供一个路径名与之关联,以FIFO文件形式存在于文件系统,因此即使是相互独立的进程只要它可以访问该路径就能彼此通过FIFO相互通信。

下面是有名管道的创建函数:

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char * pathname, mode_t mode)

第一个参数即为路径名,第二个参数为文件属性,包括打开方式、访问权限等,Linux下有很多函数使用该类型的参数,如参数值“O_CREAT | O_EXCL | 0666“表示只能创建新的文件,若文件存在则返回EEXIST错误,”0666“表示全部的读写权限;而“O_CREAT | 0666“表示创建或获取已存在的文件。与管道相比,命名管道创建后还需要一个打开操作。

3.3 信号

信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身。信号相当于软件中断,如Linux终端有一个程序正在运行,此时按下中断键(Ctrl+C)便可中断此程序的运行,即向该程序的进程发送了一个中断信号(SIGINT),该进程收到信号后,终止运行。类似Linux还有很多种类的信号,代表不同类型的通知信息,可以利用Linux命令kill –l查看操作系统支持的信号类型。

3.4 消息队列

消息队列是消息的链接表,存放在内核中并由消息队列标识符(Queue ID)标识,有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

以下是消息队列的创建、消息的发送和接收的函数:

#include <sys/msg.h>
int msgget(key_t key, int flag);
int msgsnd(int msgid, const void *ptr, size_t nbytes, int flag);
size_t msgrcv(int msgid, void *ptr, size_t nbytes, long type, int flag);

创建消息队列时需要指定一个key值,通常情况下,该key值通过ftok函数得到。ftok原型为key_t ftok( char * fname, int id ),其中fname就是指定的文件名,id是子序号。ftok函数返回一个key_t的结构,然后调用msgget函数返回一个Queue ID,至于flag参数与上面介绍的命名管道的mode参数类似,指定创建方式及访问权限等,只是参数中的常量发生相应的变化,如O_CREAT变为IPC_CREAT等。

消息的发送和接收函数中前三个参数分别为消息队列的标识符、消息缓冲区结构的指针、消息的大小,flag表示消息队列为空的时候进行接收消息及满的时候进行发送消息时是否发生阻塞,flag为0则发生阻塞,否则返回错误,type表示从消息队列内接收的消息形态,type为0表示消息队列中的所有消息都会被接收。

 

3.5 共享存储

使得多个进程可以访问同一块内存空间,即两个进程之外的内存块,两个进程均可以访问它,是最快的可用IPC方式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。

3.6 网络IPC

以上介绍的IPC通信方式是用于同一计算机上不同进程间的,而不同计算机间进程的相互通信即网络IPC:套接字socket。相信很多人对此并不陌生,socket一般用于网络通信,可认为它是利用网间通信设施实现进程间通信。进程通信之前,双方各自创建一个端点,否则无法建立联系更不能进行通信。一个完整的socket有网络协议、网络地址、网络端口三个属性,然后由操作系统为它分配一个本地唯一的socket号。就比如日常生活中的电话,双方必须都要拥有一台电话机而且号码必须唯一,建立通话时必须要知道对方的号码,否则无法进行连接。


4.小结:

本文先对Linux下的进程、线程及多进程、多线程进行了简要的介绍和比较,然后实例说明它们实际的使用方法,如何有效地满足并发处理需求,并降低模块间的耦合度。然后对Linux多线程编程及几种线程同步机制作了一定的叙述,使读者能够对Linux多线程编程有大致的认识。最后介绍了IPC通信的几种方式,重点介绍了比较常用的管道、消息队列等通信方式,并举例介绍了消息队列在多进程模式下的使用方法,对一些不太常用的通信机制只作了简单的描述,能留给读者大致的印象即可。

Linux进程与线程编程并非我们想象中的那么高深莫测,只要我们深入理解相关的概念,然后熟悉相关的系统函数,我们一样能够灵活地运用它们解决实际中的各种并发及同步问题。当然本文主要只介绍一些常用的技术及函数,而Linux中相关的高级的概念及函数也不在少数,有待读者进一步作深入的探究。


参考文献:
UNIX环境高级编程,作者:W.Richard Stevens,译者:尤晋元等,机械工业出版社。
Linux环境进程间通信,作者:郑彦兴,国防科大计算机学院。

Linux 进程 多进程 线程 多线程 同步 IPC

分享到:
评论加载中,请稍后...
创APP如搭积木 - 创意无限,梦想即时!
回到顶部