Linux下的多进程编程初步

1序言

对于没有接触过Unix/Linux操作系统的人来说,fork是最难理解的概念之一:它执行一次却返回两个值。fork函数是Unix系统最杰出的成就之一,它是七十年代UNIX初期的开发者经过常年在理论和实践上的坚苦探求后取得的成果,一方面,它使操作系统在进程管理上付出了最小的代价,另一方面,又为程序员提供了一个简约明了的多进程技巧。与DOS和初期的Windows不同,Unix/Linux系统是真正实现多任务操作的系统,可以说,不使用多进程编程,就不能算是真正的Linux环境下编程。

多线程程序设计的概念早在六十年代就被提出,但直至八十年代中期,Unix系统中才引入多线程机制,现在,因为自身的许多优点,多线程编程早已得到了广泛的应用。

下边,我们将介绍在Linux下编撰多进程和多线程程序的一些初步知识。

2多进程编程

哪些是一个进程?进程这个概念是针对系统而不是针对用户的,对用户来说,他面对的概念是程序。当用户敲入命令执行一个程序的时侯,对系统而言,它将启动一个进程。但和程序不同的是,在这个进程中,系统可能须要再启动一个或多个进程来完成独立的多个任务。多进程编程的主要内容包括进程控制和进程间通讯,在了解那些之前,我们先要简单晓得进程的结构。

2.1Linux下进程的结构

Linux下一个进程在显存里有三部份的数据,就是”代码段”、”堆栈段”和”数据段”。虽然学过汇编语言的人一定晓得linux arg,通常的CPU都有上述三种段寄存器,以便捷操作系统的运行。这三个部份也是构成一个完整的执行序列的必要的部份。

“代码段”,顾名思义,就是储存了程序代码的数据,如果机器中有数个进程运行相同的一个程序,这么它们就可以使用相同的代码段。”堆栈段”储存的就是子程序的返回地址、子程序的参数以及程序的局部变量。而数据段则储存程序的全局变量,常数以及动态数据分配的数据空间(例如用malloc之类的函数取得的空间)。这其中有许多细节问题,这儿限于篇幅就不多介绍了。系统若果同时运行数个相同的程序,它们之间就不能使用同一个堆栈段和数据段。

2.2Linux下的进程控制

在传统的Unix环境下,有两个基本的操作用于创建和更改进程:函数fork()拿来创建一个新的进程,该进程几乎是当前进程的一个完全拷贝;函数族exec()拿来启动另外的进程以替代当前运行的进程。Linux的进程控制和传统的Unix进程控制基本一致,只在一些细节的地方有些区别,比如在Linux系统中调用vfork和fork完全相同,而在有些版本的Unix系统中,vfork调用有不同的功能。因为这种差距几乎不影响我们大多数的编程,在这儿我们不予考虑。

2.2.1fork()

fork在英语中是”分叉”的意思。为何取这个名子呢?由于一个进程在运行中,假如使用了fork,就形成了另一个进程,于是进程就”分叉”了,所以这个名子取得很形象。下边就瞧瞧怎么具体使用fork,这段程序演示了使用fork的基本框架:

linux arg_linux arg_linux arg

linux arg_linux arg_linux arg

程序运行后,你还能见到屏幕上交替出现子进程与父进程各复印出的一千条信息了。假如程序还在运行中,你用ps命令才能看见系统中有两个它在运行了。

这么调用这个fork函数时发生了哪些呢?fork函数启动一个新的进程,上面我们说过,这个进程几乎是当前进程的一个拷贝:子进程和父进程使用相同的代码段;子进程复制父进程的堆栈段和数据段。这样,父进程的所有数据都可以留给子进程,而且,子进程一旦开始运行,尽管它承继了父进程的一切数据,但实际上数据却早已分开,互相之间不再有影响了,也就是说,它们之间不再共享任何数据了。它们再要交互信息时,只有通过进程间通讯来实现,这将是我们下边的内容。既然它们这么相像,系统怎样来分辨它们呢?这是由函数的返回值来决定的。对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零。在操作系统中,我们用ps函数就可以看见不同的进程号,对父进程而言,它的进程号是由比它更低层的系统调用赋于的,而对于子进程而言,它的进程号即是fork函数对父进程的返回值。在程序设计中,父进程和子进程都要调用函数fork()下边的代码,而我们就是借助fork()函数对兄妹进程的不同返回值用if…else…句子来实现让母子进程完成不同的功能,正如我们前面举的反例一样。我们看见,里面事例执行时两条信息是交互无规则的复印下来的,这是兄妹进程独立执行的结果,尽管我们的代码虽然和串行的代码没有哪些区别。

读者或许会问linux之家,假若一个大程序在运行中,它的数据段和堆栈都很大,一次fork就要复制一次,这么fork的系统开支不是很大吗?虽然UNIX自有其解决的办法,你们晓得,通常CPU都是以”页”为单位来分配显存空间的,每一个页都是实际化学显存的一个映像,象INTEL的CPU,其二页在一般情况下是4086字节大小,而无论是数据段还是堆栈段都是由许多”页”构成的,fork函数复制这两个段,只是”逻辑”上的,并非”数学”上的,也就是说,实际执行fork时,数学空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区别,系统就将有区别的”页”从数学上也分开。系统在空间上的开支就可以达到最小。

下边演示一个足以”搞死”Linux的小程序,其源代码十分简单:

linux arg_linux arg_linux arg

这个程序哪些也不做,就是死循环地fork,其结果是程序不断形成进程,而这种进程又不断形成新的进程,很快,系统的进程就满了,系统就被如此多不断形成的进程”撑死了”。其实只要系统管理员预先给每位用户设置可运行的最大进程数,这个恶意的程序就完成不了试图了。

2.2.2exec()函数族

下边我们来瞧瞧一个进程怎样来启动另一个程序的执行。在Linux中要使用exec函数族。系统调用execve()对当前进程进行替换,替换者为一个指定的程序,其参数包括文件名(filename)、参数列表(argv)以及环境变量(envp)。exec函数族其实不止一个,但它们大致相同,在Linux中,它们分别是:execl,execlp,execle,execv,execve和execvp,下边我只以execlp为例,其它函数到底与execlp有何区别,请通过manexec命令来了解它们的具体情况。

一个进程一旦调用exec类函数,它本身就”死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,惟一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过早已是另一个程序了。(不过exec类函数中有的还容许承继环境变量之类的信息。)

这么假如我的程序想启动另一程序的执行但自己仍想继续运行的话,怎样办呢?那就是结合fork与exec的使用。下边一段代码显示怎么启动运行其它程序:

linux arg_linux arg_linux arg

此程序从终端读入命令并执行之,执行完成后linux arg,父进程继续等待从终端读入命令。熟悉DOS和WINDOWS系统调用的同事一定晓得DOS/WINDOWS也有exec类函数,其使用方式是类似的,但DOS/WINDOWS还有spawn类函数,由于DOS是单任务的系统,它只能将”父进程”留驻在机器内再执行”子进程”,这就是spawn类的函数。WIN32早已是多任务的系统了,但还保留了spawn类函数,WIN32中实现spawn函数的方式同前述UNIX中的方式差不多,开办子进程后父进程等待子进程结束后才继续运行。UNIX在其四开始就是多任务的系统,所以从核心角度上讲不须要spawn类函数。

在这一节里,我们还要讲讲system()和popen()函数。system()函数先调用fork(),之后再调用exec()来执行用户的登陆shell,通过它来查找可执行文件的命令并剖析参数,最后它么使用wait()函数族之一来等待子进程的结束。函数popen()和函数system()相像,不同的是它调用pipe()函数创建一个管线,通过它来完成程序的标准输入和标准输出。这两个函数是为这些不太勤快的程序员设计的,在效率和安全方面都有相当的缺陷,在可能的情况下,应当尽量避开。

2.3Linux下的进程间通讯

linux arg_linux arg_linux arg

详尽的述说进程间通讯在这儿绝对是不可能的事情,但是笔者很难有信心说自己对这一部份内容的认识达到了哪些样的地步,所以在这一节的开头首先向你们推荐知名作者RichardStevens的知名作品:《AdvancedProgrammingintheUNIXEnvironment》,它的英文译本《UNIX环境中级编程》已有机械工业出版社出版,原文精彩,译文同样地道,假如你的确对在Linux下编程有浓烈的兴趣,这么赶忙将这本书摆到你的桌子上或计算机后面来。说那么多实在是难抑心里的崇敬之情,言归正传,在这一节里,我们将介绍进程间通讯最最初步和最最简单的一些知识和概念。

首先,进程间通讯起码可以通过传送打开文件来实现,不同的进程通过一个或多个文件来传递信息,事实上,在好多应用系统里,都使用了这些技巧。但通常说来,进程间通讯(IPC:InterProcessCommunication)不包括这些其实比较低级的通讯方式。Unix系统中实现进程间通讯的方式好多,但是不幸的是,很少方式能在所有的Unix系统中进行移植(惟一一种是半双工的管线,这也是最原始的一种通讯方法)。而Linux作为一种新兴的操作系统,几乎支持所有的Unix下常用的进程间通讯方式:管线、消息队列、共享显存、信号量、套插口等等。下边我们将逐一介绍。

2.3.1管线

管线是进程间通讯中最古老的形式,它包括无名管线和有名管线两种,后者用于父进程和子进程间的通讯,前者用于运行于同一台机器上的任意两个进程间的通讯。

无名管线由pipe()函数创建:

#include

intpipe(intfiledis[2]);

参数filedis返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。filedes[1]的输出是filedes[0]的输入。下边的事例示范了怎样在父进程和子进程间实现通讯。

linux arg_linux arg_linux arg

在Linux系统下,有名管线可由两种方法创建:命令行形式mknod系统调用和函数mkfifo。下边的两种途径都在当前目录下生成了一个名为myfifo的有名管线:

方法一:mkfifo(“myfifo”,”rw”);

方法二:mknodmyfifop

生成了有名管线后,就可以使用通常的文件I/O函数如open、close、read、write等来对它进行操作。下边即是一个简单的事例,假定我们早已创建了一个名为myfifo的有名管路。

linux arg_linux arg_linux arg

2.3.2消息队列

linux arg_linux arg_linux arg

消息队列用于运行于同一台机器上的进程间通讯,它和管路很相像,事实上,它是一种正逐步被淘汰的通讯方法,我们可以用流管线或则套插口的方法来代替它,所以,我们对此方法也不再解释,也建议读者忽视这些形式。

2.3.3共享显存

共享显存是运行在同一台机器上的进程间通讯最快的方法,由于数据不须要在不同的进程间复制。一般由一个进程创建一块共享显存区,其余进程对这块显存区进行读写。得到共享显存有两种形式:映射/dev/mem设备和显存映像文件。前一种方法不给系统带来额外的开支,但在现实中并不常用,由于它控制存取的将是实际的化学显存,在Linux系统下,这只有通过限制Linux系统存取的显存才可以做到,这其实不太实际。常用的方法是通过shmXXX函数族来实现借助共享显存进行储存的。

首先要用的函数是shmget,它获得一个共享储存标示符。

#include

#include

#include

intshmget(key_tkey,intsize,intflag);

这个函数有点类似你们熟悉的malloc函数,系统根据恳求分配size大小的显存用作共享显存。Linux系统内核中每位IPC结构都有的一个非负整数的标示符,这样对一个消息队列发送消息时只要引用标示符就可以了。这个标示符是内核由IPC结构的关键字得到的,这个关键字,就是前面第一个函数的key。数据类型key_t是在头文件sys/types.h中定义的,它是一个长整形的数据。在我们前面的章节中,都会遇到这个关键字。

当共享显存创建后,其余进程可以调用shmat()将其联接到自身的地址空间中。

void*shmat(intshmid,void*addr,intflag);

shmid为shmget函数返回的共享储存标示符,addr和flag参数决定了以哪些方法来确定联接的地址,函数的返回值即是该进程数据段所联接的实际地址,进程可以对此进程进行读写操作。

使用共享储存来实现进程间通讯的注意点是对数据存取的同步,必须确保当一个进程去读取数据时,它所想要的数据早已写好了。一般,讯号量被要来实现对共享储存数据存取的同步,另外,可以通过使用shmctl函数设置共享储存显存的个别标志位如SHM_LOCK、SHM_UNLOCK等来实现。

2.3.4讯号量

linux arg_linux arg_linux arg

讯号量又称为讯号灯,它是拿来协调不同进程间的数据对象的,而最主要的应用是前一节的共享显存形式的进程间通讯。本质上,讯号量是一个计数器,它拿来记录对某个资源(如共享显存)的存取状况。通常说来,为了获得共享资源,进程须要执行下述操作:

(1)测试控制该资源的讯号量。

(2)若此讯号量的值为正,则容许进行使用该资源。进程将进号量减1。

(3)若此讯号量为0,则该资源目前不可用,进程步入睡眠状态,直到讯号量值小于0,进程被唤起,转到步骤(1)。

(4)当进程不再使用一个讯号量控制的资源时,讯号量值加1。若果此时有进程正在睡眠等待此讯号量,则唤起此进程。

维护讯号量状态的是Linux内核操作系统而不是用户进程。我们可以从头文件/usr/src/linux/include/linux/sem.h中见到内核拿来维护讯号量状态的各个结构的定义。讯号量是一个数据集合,用户可以单独使用这一集合的每位元素。要调用的第一个函数是semget,用以获得一个讯号量ID。

linux arg_linux arg_linux arg

key是上面讲过的IPC结构的关键字,它将来决定是创建新的讯号量集合,还是引用一个现有的讯号量集合。nsems是该集合中的讯号量数。若果是创建新集合(通常在服务器中),则必须指定nsems;若果是引用一个现有的讯号量集合(通常在顾客机中)则将nsems指定为0。

semctl函数拿来对讯号量进行操作。

intsemctl(intsemid,intsemnum,intcmd,unionsemunarg);

不同的操作是通过cmd参数来实现的,在头文件sem.h中定义了7种不同的操作,实际编程时可以参照使用。

semop函数手动执行讯号量集合上的操作字段。

intsemop(intsemid,structsembufsemoparray[],size_tnops);

semoparray是一个表针,它指向一个讯号量操作字段。nops规定该字段中操作的数目。

下边,我们看一个具体的事例,它创建一个特定的IPC结构的关键字和一个讯号量,完善此讯号量的索引,更改索引指向的讯号量的值,最后我们消除讯号量。在下边的代码中,函数ftok生成我们上文所说的惟一的IPC关键字。

linux arg_linux arg_linux arg

2.3.5套插口

linux arg_linux arg_linux arg

套插口(socket)编程是实现Linux系统和其他大多数操作系统中进程间通讯的主要形式之一。我们熟知的WWW服务、FTP服务、TELNET服务等都是基于套插口编程来实现的。不仅在异地的计算机进程间以外,套插口同样适用于本地同一台计算机内部的进程间通讯。关于套插口的精典教材同样是RichardStevens编绘的《Unix网路编程:联网的API和套接字》,复旦学院出版社出版了该书的翻印版。它同样是Linux程序员的必备书籍之一。

关于这一部份的内容,可以参照本文作者的另一篇文章《设计自己的网路蚂蚁》,那儿由常用的几个套插口函数的介绍和示例程序。这一部份其实是Linux进程间通讯编程中最须关注和最吸引人的一部份,虽然,Internet正在我们身边以不可思议的速率发展着红旗linux6.0教程,假若一个程序员在设计编撰他下一个程序的时侯,根本没有考虑到网路,考虑到Internet,这么,可以说,他的设计很难成功。

3Linux的进程和Win32的进程/线程比较

熟悉WIN32编程的人一定晓得,WIN32的进程管理方法与Linux上有着很大区别,在UNIX里,只有进程的概念,但在WIN32里却还有一个”线程”的概念,这么Linux和WIN32在这儿到底有着哪些区别呢?

WIN32里的进程/线程是承继自OS/2的。在WIN32里,”进程”是指一个程序,而”线程”是一个”进程”里的一个执行”线索”。从核心上讲,WIN32的多进程与Linux并无多大的区别,在WIN32里的线程才相当于Linux的进程,是一个实际正在执行的代码。并且,WIN32里同一个进程里各个线程之间是共享数据段的。这才是与Linux的进程最大的不同。

下边这段程序显示了WIN32下一个进程怎么启动一个线程。

linux arg_linux arg_linux arg

在WIN32下,使用CreateThread函数创建线程,与Linux下创建进程不同,WIN32线程不是从创建处开始运行的,而是由CreateThread指定一个函数,线程就从哪个函数处开始运行。此程序同上面的UNIX程序一样,由两个线程各复印1000条信息。threadID是子线程的线程号,另外,全局变量g是子线程与父线程共享的,这就是与Linux最大的不同之处。你们可以看出,WIN32的进程/线程要比Linux复杂,在Linux要实现类似WIN32的线程并不难,只要fork之后,让子进程调用ThreadProc函数,而且为全局变量开办共享数据区就行了,但在WIN32下就难以实现类似fork的功能了。所以如今WIN32下的C语言编译器所提供的库函数其实早已能兼容大多数Linux/UNIX的库函数,但却仍未能实现fork。

对于多任务系统,共享数据区是必要的,但也是一个容易造成混乱的问题,在WIN32下,一个程序员很容易忘掉线程之间的数据是共享的这一情况,一个线程更改过一个变量后,另一个线程却又更改了它,结果导致程序出问题。但在Linux下,因为变量原本并不共享,而由程序员来显式地指定要共享的数据,使程序显得更清晰与安全。

至于WIN32的”进程”概念,其涵义则是”应用程序”,也就是相当于UNIX下的exec了。

=======================================================

注:须要C/C++Linux服务器开发学习资料私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,解释器,DPDK等),免费分享

Tagged:
Author

这篇优质的内容由TA贡献而来

刘遄

《Linux就该这么学》书籍作者,RHCA认证架构师,教育学(计算机专业硕士)。

发表回复