您的位置:首页 > 运维架构 > Linux

Linux进程学习

2014-01-09 09:18 375 查看
Linux进程的基本知识和实现

最近一周学习了Linux 进程编程的知识,现对其总结如下。

在第一部分中我们先对进程的基本概念以及在Linux 中是如何来现实进程的进行介绍

Tiger-John说明:

许多人在学习中只注重如何编程,却忘了注重原理,不去深究其基本原理。其实操作系统的原理就好 比金庸武侠小说的内功一样,而所有的具体实现如:Linux操作系统,uc/os操作系统都只是武功招式而已。如果我们内功学的很好的话,再来学习具体的实现过程是很快的。而且也会对其知识有更加本质的了解。

一.进程的基本概念:

1.为什么计算机操作系统要引进进程:

在操作系统中引入进程的目的是为了使多个程序并发执行,以改善资源利用率及提高系统吞吐量。

2.进程的概念:

进程是程序的一次执行,进程是拥有资源的最小单位和调度单位(在引入线程的操作系统中,线程是最小的调度单位)

3.进程由什么组成

进程由进程控制块(PCB),数据,程序3部分组成。其中PCB是进程的灵魂。

4.进程的状态:

进程的三种最基本的状态是:运行态(running),就绪态(readying), 阻塞态(block)

5.进程和程序的区别:

进程和程序的主要区别是进程是动态的,程序是静态的。进程时运行中的程序,程序是一些保存在硬盘上的可执行的代码。

6.进程的优点和缺点

(任何事物都是有其两面性。我们在学习的时候要注意其优点和缺点。人们也就再发现事物缺点的过程中,不断的去改善它,从而引入了新的事物。在操作系统的学习过程中,我们会发现很多这样的例子。人们在不断追求完美的过程中,不断的引入新的知识点--进程和线程的出现就足可以说明这一切)

优点:使多个程序并发执行

缺点:程序并发执行时付出了巨大的时空开销,每个进程在进行切换时身上带了过多的“累赘”导致系统效率降低。

于是人们为了解决这个缺点想到让进程在并行时不拥有资源---从而引入了线程的概念:即线程本身不拥有资源或者是很少的资源,进程只是拥有资源的基本单位,线程是调度的基本单位

7.线程的引入:

在操作系统中引入线程则是为了减少程序并发执行时所付出的时空开销,使操作系统具有更好的并发性。

二.Linux中是如何具体实现进程和线程

1.在linux中通过task_struct结构体来描述进程的PCB,我们可以在include/linux/sched.h中看到对进程task_struct的定义和进程状态的描述。

1>linux中的进程状态

a.运行状态:进程正在运行或在运行队列中等待运行 。

b.可中断等待状态:进程正在等待某个事件完成(如等待数据到达)。等待过程中可以被信号或定时器唤醒。

c.不可中断等待状态:进程正在等待某个事件完成并且等待中不可以被信号或定时器唤醒,必须一直等待到事件发生。

d.僵死状态:进程已终止,但进程描述符依然存在,直到父进程调用wait()函数后释放。

e.停止状态:进程因为收到SINSTOP,SIGSTP,SIGTIN,SGIOU信号后停止运行或者该进程正在被跟踪。

Tiger-john说明:

1在include/linux/sched.h中我们可以看到Linxu中进程状态的具体实现:

#define TASK_RUNNING 0

#define TASK_INTERRUPTIBLE 1

#define TASK_UNINTERRUPTIBLE 2

#define TASK_ZOMBIE 4

#define TASK_STOPPED 8

其中:

TASK_RUNNING是就绪态,进程当前只等待CPU资源。

TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE都是阻塞态,进程当前正在等待除CPU外的其他系统资源;前者可以被信号唤醒,后者不可以。

TASK_ZOMBIE是僵尸态,进程已经结束运行,但是进程控制块尚未注销。

TASK_STOPPED是挂起状态,主要用于调试目的。进程接收到SIGSTOP信号后会进入该状态,在接收到SIGCONT后又会恢复运行。

2.我们可以在终端中通过命令ps或pstree查看当前系统中的进程

用ps命令可以查看进程的当前状态。运行状态为R,可中断等待状态为S,不可中断等待状态为D,僵死状态为Z,停止状态为T。

实例:

think@ubuntu:~$ ps -eo pid,stat

PID STAT

1 Ss

2 S

3 S

37 SN

364 Ss

371 S<

442 S<s

1060 Sl

1081 Ssl

1085 Ssl

1203 Ss+

3782 Ss

3803 R+

Tiger-John说明:

在运行结果中有一些后缀字符,其意义分别为< (高优先级进程),N(低优先级进程),L(内存锁页,即页不可以被换出内存),s(该进程为会话首进程),l(多线程进程),+(进程位于前台进程组)。

例如:Ssl说明该进程处于可中断等待状态,且该进程为会话首进程,而且是一个多线程的进程。

2.linux系统的进程间通信有哪几种方式

1>管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

2>有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

3>信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

4>消息队列( message queue ) : 消息队列是消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

5> 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

6> 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

7>套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

3.进程控制

1>linux进程控制包括创建进程,执行进程,退出进程以及改变进程优先级等。

在linux系统中,用于对进程进行控制的系统调用有:

a.fork:用于创建一个新进程。

b.exit :用于终止进程

c.exec :用于执行一个应用程序

d.wait :将父进程挂起,等待子进程终止

e.getpid :获取当前进程的进程ID

f.nice : 该变进程的优先级

4.进程标识

1>Linux操作系统中,每个进程都是通过唯一的进程ID标识的。进程ID 是一个非负数。每个进程除了进程ID外还有一些其它信息,都可以通过相应的函数获得。

2>主要的函数有:

pid_t getpid(void) :获得进程ID

pid_t getppid(void):获得进程父进程的ID

pid_t getuid(void) :获得进程的实际用户ID

pid_t geteuid(void) :获得进程的有效用户ID

pid_t getgid(void) : 获得进程的实际组ID

pid_t getegid(void) 获得进程的有效组ID

Tiger-Johen说明:

这些函数的声明在 sys/types.h和unistd.h 头文件中。

2>用户ID和组ID的相关概念

a.实际用户ID(uid) :标识运行该进程的用户

b.有效用户ID( euid): 标识以什么用户身份来运行进程。

例如:一个普通用户A,运行了一个程序,而这个程序是以root 身份来运行的,着程序运行时就具有root 权限。此时,实际用户ID时A用户的ID,而有效用户ID是root用户ID

3>函数实例:

表头文件:#include <unistd.h>

#include<sys/types.h>

函数定义:pid_t getpid(void)

函数说明:getpid()用来取得目前进程的进程识别码,许多程序利用取到的此值来建立临时文件,以避免临时文件相同带来的问题

返回值:目前进程的进程识别码

函数实例:

#include<stdio.h>

#include<sys/types.h>

#include <unistd.h>

main()

{

printf("pid = %d/n", getpid());

}

三.进程的内存映像

1.Linux下程序转化成进程

a.Linux下C程序的生成分为4个阶段:

预编译

编译

汇编

链接

Tiger-Johen说明:

编译器gcc进过预编译,编译,汇编3个步骤将源程序文件转换为目标文件。

b.当程序执行时,操作系统将可执行程序复制到内存中。程序转化为进程通常需要经过以下步骤:

内核将程序读入内存,为程序分配内存空间

内核为该进程分配进程标识符(PID)和其他资源

内核为该进程保存PID及相应的状态信息,把进程放到运行队列中等待执行。程序转化为进程后就可以被操作系统的调度程序执行了。

2.进程的内存映像

a.进程的内存映像是指内核在内存中如何存放可执行程序文件。在将程序转化为进程的过程中,操作系统将可执行程序由硬盘复制到内存中。

b.linux下程序映像的一般布局如下:(从低地址到高地址)

1>代码段:代码段是只读的,可被多个进程共享。

2>数据段: 存储已被初始化的变量,包括全局变量和已被初始化的静态变量。

3>未初始化数据段:存储未被初始化的静态变量,它也被称为bss段

4>堆:用于存放程序运行中动态分配的变量

5>栈:用户函数调用,保存函数的返回地址,函数的参数,函数内部定义的局部变量。

Tiger-Johen说明:

可执行程序和内存映像的区别:

a.可执行程序位于磁盘中而内存映像位于内存中;

b.可执行程序没有堆栈,因为程序 被加载到内存中才会分配堆栈;

c.可执行程序虽然也有未初始化数据段但它并不被储存在位于硬盘中的可执行文件中;

d.可执行程序时静态的,不变的,而内存映像随着程序的执行时在动态变化的

fork()和vfork()的学习

通过上一部分的学习,我们了解了进程的概念以及在Linux中进程的实现,此部分我们将具体学习如何在Linux中创建一个进程。

一前言:

通过原理知识的学习,我们知道每个进程由进程ID号标识。进程被创建时系统会为其分配一个唯一的进程ID号。当一个进程向其父进程(创建该进程的进程)传递其终止消息时,意味这个进程的整个生命周期结束。此时,该进程占用的所用资源包括进程ID被全部释放。

那么在Linux中如何创建一个进程呢?

创建进程有两种方式:一是由操作系统创建,二是由父进程创建的进程(通常为子进程)。

系统调用fork是创建一个新进程的唯一方式。vfork也可创建进程,但它实际上还是调用了fork函数。

Tiger-John 说明:

1.由操作系统创建的进程它们之间是平等的不存在资源继承关系。

2.由父进程创建的进程通常为子进程它们之间有继承关系。

3.在系统启动时,OS会创建一些进程,它们承担着管理和分配系统资源的任务,即系统进程。

如0号idle进程,它是从无到有诞生的第一个线程,主要用于节能;关于idle进程,系统最初引导0号进程,对应的PCB为init_task(),要说明下它是0号进程PCB的头,并不是1号init进程,在引导结束后即成为cpu 上的idle进程。在每个cpu上都有一个idle进程,这些进程登记在init_tasks[]数组中。idle进程不进入就绪队列,系统稳定后,仅当就绪队列为空的时候idle进程才会被调度到,在没有其它进程运行的情况下,它大量时间占用cpu。

1号进程(init进程)它是一个由内核启动的用户级进程,它是所有用户进程的父进程。实际上,Linux2.6在初始化阶段首先把它建立为一个内核线程kernel_init:

kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);

参数CLONE_FS | CLONE_FILES | CLONE_SIGHAND表示0号线程和1号线程分别共享文件系统(CLONE_FS)、打开的文件(CLONE_FILES)和信号处理程序(CLONE_SIGHAND)。当调度程序选择到kernel_init内核线程时,kernel_init就开始执行内核的一些初始化函数将系统初始化。

那么,kernel_init()内核线程是怎样变为用户进程的呢?

实际上,kernel_init()内核函数中调用了execve()系统调用,该系统调用装入用户态下的可执行程序init(/sbin/init)。注意,内核函数kernel_init()和用户态下的可执行文件init是不同的代码,处于不同的位置,也运行在不同的状态,因此,init是内核线程启动起来的一个普通的进程,这也是用户态下的第一个进程。init进程从不终止,因为它创建和监控操作系统外层所有进程的活动。

二fork()函数和vfork()函数的学习

1.fork()函数

调用fork函数后,当前进程分裂为两个进程,一个是原来的父进程,另一个是刚创建的子进程。父进程调用fork后返回值是子进程的ID,子进程中返回值是0,若进程创建失败,只返回-1。失败原因一般是父进程拥有的子进程个数超过了规定限制(返回EAGAIN)或者内存不足(返回ENOMEM)。我们可以依据返回值判断进程,一般情况下调用fork函数后父子进程谁先执行是未定的,取决于内核所使用的调度算法。一般情况下os让所有进程享有同等执行权,除非某些进程优先级高。若有一个孤儿进程,即父进程先于子进程死去,子进程将会由init进程收养。

函数实例:通过fork()函数创建一个进程:

1 #include<sys/types.h>

2 #include<unistd.h>

3 #include<stdio.h>

4

5 main()

6 {

7 pid_t pid;

8 printf("PID before fork() :%d/n",(int)getpid());

9

10 pid = fork();

11 if(pid < 0){

12 printf("error in fork!/n");

13 }

14 else if(0 == pid){

15 printf("I'm the child process, CurpPID is %d,ParentPid is %d /n",pid,(int)getppid());

16 }

17 else{

18 printf("I'm the parent process,child PID is %d,ParentPID is %d/n",pid,(int)getpid());

19 }

20

程序经过调试后结果如下:

think@ubuntu:~/work/process_thread/fork$ ./fork

PID before fork() :4566

I'm the parent process,child PID is 4567,ParentPID is 4566

I'm the child process, CurpPID is 0,ParentPid is 4566

从程序执行结果可以看出:调后fork()函数后返回两个值,子进程返回值为0,而父进程的返回值为创建的子进程的进程ID。

Tiger-John说明:

1>Linux进程一般包括代码段,数据段和堆栈段。代码段存放程序的可执行代码;数据段存放程序的全局变量、常量、静态变量;堆存放动态分配的内存变量,栈用于函数调用,存放函数参数、函数内部定义的局部变量。

2>有人说调用fork函数后,fork()函数返回了两个值,这是一中错误的说法。其实,当系统调用fork()函数后fork()会将调用进程的所有内容原封不动的拷贝到新产生的子进程中去,当前进程分裂成了两个进程分别在执行,互不干扰。

3>.看一个函数实例来仔细体会一下系统调用fork()函数后是如何执行的。

1 #include<stdio.h>

2 #include<unistd.h>

3 #include<sys/types.h>

4

5 int main()

6 {

7 pid_t pid;

8 int count = 0;

9 pid = fork();

10

11 printf("This is first time,pid = %d/n",pid);

12 printf("This is the second time,pid = %d/n",pid);

13 count++;

14 printf("count = %d/n",count);

15

16 if(pid > 0){

17 printf("This is the parent process,the child has the pid :%d /n",pid);

18 }

19 else if(!pid){

20 printf("This is the child process./n");

21 }

22 else{

23 printf("fork failed./n");

24 }

25

26 printf("This is third,pid = %d/n",pid);

27 printf("This is four time,pid = %d/n",pid);

28 return 0;

29 }

程序经过调试后结果

think@ubuntu:~/work/process_thread/fork1$ ./fork

This is first time,pid = 4614

This is the second time,pid = 4614

count = 1

This is the parent process,the child has the pid :4614

This is third,pid = 4614

This is four time,pid = 4614

This is first time,pid = 0

This is the second time,pid = 0

count = 1

This is the child process.

This is third,pid = 0

This is four time,pid = 0

think@ubuntu:~/work/process_thread/fork1$

Tiger-John说明:

从上面的程序的执行结果我们看到一个奇怪的现象:为什么printf的语句执行两次,

而那句“count++;”的语句却只执行了一次?

系统在调用fork()后分裂成了两个函数分别执行,互不干扰。

2.Vfork和fork的区别:

1>vfork也可创建进程,但它实际上还是调用了fork函数。

2>在说明他们的区别之前我们先看两个程序和执行结果

函数实例1:用fork()函数创建进程

1 #include<stdio.h>

2 #include<sys/types.h>

3 #include<unistd.h>

4 #include<stdlib.h>

5 int globVar = 5;

6

7 int main(void)

8 {

9 pid_t pid;

10 int var = 1;

11 int i;

12 printf("fork is different with vfork /n");

13

14 pid = fork();

15 if(!pid){

16 i=3;

17 while(i-- > 0){

18 printf("Child process is running/n");

19 globVar++;

20 var++;

21 sleep(1);

22 }

3 printf("Child's globVar = %d,var = %d/n",globVar,var);

24 }

25 else if(pid){

26 i=5;

27 while(i-- > 0){

28 printf("Parent process is running/n");

29 globVar++;

30 var++;

31 sleep(1);

32 }

33 printf("Parent's globVar = %d,var %d/n",globVar,var);

34 exit(0);

35 }

36 else{

37 perror("Process creation failed/n");

38 exit(-1);

39 }

40 }

程序经过调试后;

think@ubuntu:~/work/process_thread/fork3$ ./fork

fork is different with vfork

Parent process is running

Child process is running

Child process is running

Parent process is running

Child process is running

Parent process is running

Child's globVar = 8,var = 4

Parent process is running

Parent process is running

Parent's globVar = 10,var = 6

函数实例2:用vfork()函数创建一个进程

1 #include<stdio.h>

2 #include<sys/types.h>

3 #include<unistd.h>

4 #include<stdlib.h>

5 int globVar = 5;

6 int main(void)

7 {

8 pid_t pid;

9 int var = 1;

10 int i;

11

12 printf("fork is different with vfork!/n");

13

14 pid = vfork();

15 if(!pid){

16 i=3;

17 while(i-- > 0)

18 {

19 printf("Child process is running/n");

20 globVar++;

21 var++;

22 sleep(1);

23 }

24 printf("Child's globVar = %d,var =%d/n",globVar,var);

25 }

26 else if(pid){

27 i = 5;

28 while(i-- > 0)

29 {

30 printf("Parent process is running/n");

31 globVar++;

32 var++;

33 sleep(1);

34 }

35 printf("Parent's globVar = %d,var %d/n",globVar,var);

36 exit(0);

37 }

38 else {

39 perror("Process creation failed/n");

40 exit(0);

41 }

42

43 }

程序经过调试后结果为:

think@ubuntu:~/work/process_thread/fork3$ ./vfork

fork is different with vfork!

Child process is running

Child process is running

Child process is running

Child's globVar = 8,var = 4

Parent process is running

Parent process is running

Parent process is running

Parent process is running

Parent process is running

Parent's globVar = 13,var = 5

Tiger-John说明:

我们通过以上两个函数的执行可以看到一些区别:

1.使用fork创建子进程时,子进程继承了父进程的全局变量和局部变量。在子进程中,最后全局变量globVar和局部变量var的值均递增3,分别为8和4.不管是全局变量还是局部变量,子进程与父进程对它们的修改互不影响。

2.父进程中两者分别递增5.最后结果为10和6

通过以上程序的运行结果可以证明fork的子进程有自己独立的地址空间。

3.子进程和父进程的执行顺序是很随意的,没有固定的顺序。父子进程的输出是混杂在一起的。

--------------------------------------------

1.用vfork()函数创建子进程后,父进程中globVar和var最后均递增了8.这是因为vfork的子进程共享父进程的地址空间,子进程修改变量对父进程是可见的。

2.使用vfork()函数子进程后打印的结果是子进程在前,父进程在后,说明vfork()保证子进程先执行,在子进程调用exit获exec之前父进程处于阻塞等待状态。

3>那么现在来看看fork()和vfork()函数之间的区别:

(1)fork():使用fork()创建一个子进程时,子进程只是完全复制父进程的资源。这样得到的子进程独立于父进程具有良好的并发性。

vfork(): 使用 vfor创建一个子进程时,操作系统并不将父进程的地址空间完全复制到子进程。而是子进程共享父进程的地址空间,即子进程完全运行在父进程的地址空间上。子进程对该地址空间中任何数据的修改同样为父进程所见。

(2)fork():父子进程执行顺序不定;

vfork():保证子进程先运行,在调用exec或exit之前与父进程共享数据,在它调用exec或exit之后父进程才可能被调度运行。

(3)vfork保证子进程先运行,在它调用exec或exit后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

孤儿进程和守护进程

通过前面的学习我们了解了如何通过fork()函数和vfork()函数来创建一个进程。现在 我们继续深入来学习两个特殊的进程:孤儿进程和守护进程

一.孤儿进程

1.什么是 孤儿进程

如果一个子进程的父进程先于子进程 结束, 子进程就成为一个孤儿进程,它由 init 进程收养,成为 init 进程的子进程。

2.那么如何让一个进程变为一个孤儿进程呢?

我们可以先创建一个进程,然后杀死其父进程,则其就变成了孤儿进程。

pid = fork();

if(pid > 0) {

exit(0);

}

3. 函数实例:

1 #include<stdio.h>

2 #include<unistd.h>

3 #include<sys/types.h>

4 #include<stdlib.h>

5

6 int main()

7 {

8 pid_t pid;

9 pid = fork();

10 if(!pid){

11 while(1){

12 printf("A background process,PID:%d/n,ParentID:%d/n" ,getpid(),getppid());

13 sleep(3);

14

15 }

16 }

17 else if(pid > 0){

18 printf("I am parent process,my pid is %d/n",getpid() );

19 exit(0);

20 }

21 else {

22 printf("Process creation failed!/n");

23 }

24 return 0;

25

26 }

程序运行结果

I am parent process,my pid is 2026

A background process,PID:2027

,ParentID:2026

think@ubuntu:~/work/process_thread/fork2$ A background process,PID:2027

,ParentID:1

A background process,PID:2027

,ParentID:1

A background process,PID:2027

,ParentID:1

A background process,PID:2027

,ParentID:1

A background process,PID:2027

,ParentID:1

A background process,PID:2027

,ParentID:1

Tiger-John说明:

通过以上方法,就可以实现把一个进程变为孤儿进程 。 当要结束一个孤儿进程时只能在终端输入命令: kill 2027(kill 孤儿进程号)来结束其运行。

二守护进程

1 . 什么是守护进程呢?

( daemon) 是指在后台运行,没有控制终端与之相连的进程。它独立于控制终端,通常周期性地执行某种任务 。

Tiger-John说明:那么,守护进程为什么要脱离后台去运行呢?

守护进程脱离于终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息所打断

2. 为什么要引入守护进程:

由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能够突破这种限制,它从被执行开始运转,直到整个系统关闭时才退出。如果想让某个进程不因为用户或终端或其他地变化而受到影响,那么就必须把这个进程变成一个守护进程。

3 .守护进程的特性

1>守护进程最重要的特性是后台运行 。

2>其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是 shell )中继承下来的。

3>最后,守护进程的启动方式有其特殊之处。它可以在 Linux 系统启动时从启动脚本 /etc/rc.d 中启动,可以由作业规划进程 crond 启动,还可以由用户终端(通常是 shell )执行。

4. 守护进程的启动方式有多种:

a. 它可以在 Linux 系统启动时从启动脚本 /etc/rc.d 中启动

b. 可以由作业规划进程 crond 启动;

c. 还可以由用户终端(通常是 shell )执行。

Tiger-John 总结:

 守护进程是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程常常在系统引导装入时启动,在系统关闭时终止。 Linux 系统有很多守护进程,大多数服务都是通过守护进程实现的,同时,守护进程还能完成许多系统任务,例如,作业规划进程 crond 、打印进程 lqd 等(这里的结尾字母 d 就是 Daemon 的意思)。

5. 如何编写守护进程呢

第一步:创建子进程,父进程退出

1>. 由于守护进程是脱离控制终端的,因此,完成第一步后就会在 Shell 终端里造成一程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在 Shell 终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离。

2> 在 Linux 中父进程先于子进程退出会造成子进程成为孤儿进程,而每当系统发现一个孤儿进程时就会由 1 号进程( init) 收养它。

方法是调用 fork 产生一个子进程,然后使得父进程退出

pid = fork();

if( 0 == pid)

exit(0); // 如果是父进程,就结束父进程,子进程结束。

第二步:在子进程中创建新会话:

这个步骤是创建守护进程中最重要的一步,使用系统函数 setsid

Tiger-John 补充: 几个相关概念

a. 进程组:是一个或多个进程的集合。进程组有进程组 ID 来唯一标识。除了进程号( PID )之外,进程组 ID ( GID) 也是一个进程的必备属性。每个进程组都有一个组长进程,其组长进程的进程号等于进程组 ID 。且该进程组 ID 不会因组长进程的退出而受到影响。

b. 会话周期:会话期是一个或多个进程组的集合。通常,一个会话开始与用户登录,终止于用户退出,在此期间该用户运行的所用进程都属于这个会话期。

c. 登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。

Tiger-John 说明: 为什么要涉及它们呢?

因为控制终端,登录会话和进程组通常是从父进程继承下来的。我们就是要摆脱它们,使之不受它们的影响。

那么如何去实现呢,此时我们在第一步的基础上可以调用 setsid ()函数。

1>setsid 函数用于创建一个新的会话,并担任该会话组的组长。调用 setsid 有下面的 3 个作用:

让进程摆脱原会话的控制

让进程摆脱原进程组的控制

让进程摆脱原进程组的控制

让进程摆脱原控制终端的控制

2>. 在创建守护进程时为什么要调用 setsid 函数呢?

由于创建守护进程的第一步调用了 fork 函数来创建子进程,再将父进程退出。由于在调用了 fork 函数时,子进程全盘拷贝了父进程的会话期,进程组,控制终端等,虽然父进程退出了,但会话期,进程组,控制终端等并没有改变,因此,还不是真正意义上的独立开来,而 setsid 函数能够使进程完全独立出来,从而摆脱其他进程的控制。

Tiger-John 说明:

a. 当进程组是会话组长时 setsid() 调用失败。但是通过第一步已经保证了进程不是会话组长。

b.setsid( )调用成功后,进程成为新的会话组长和新的进程组长,并于原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。

c. 此时我们还要禁止进程重新打开控制终端

进程虽然已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:

那么如何实现呢?

我们可以再次建立一个子进程,退出父进程,保证该进程不是进程组长,同时让该进程无法再打开一个新的终端

pid = fork() ;

exit(0) ;

第三步:改变当前目录为根目录

1>使用 fork 创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统是不能卸载的,这对以后的使用会造成很多的不便。因此,我们一般是让” /” 作为守护进程的当前工作目录,这样就可以避免上述的问题。如果有特殊需要,也可以把当前工作目录换成其他的路径。

2>改变工作目录的常见函数是 chdir().

第四步:重设文件权限掩码

1>文件权限掩码是指屏蔽掉文件权限中的对应位。由于使用 fork 函数新建的子进程继承了父进程的文件权限掩码,这就给子进程使用文件带来了很多的麻烦。因此,把文件权限掩码设置为 0 ,可以很大程度上增强该守护进程的灵活性。

2>设置文件权限掩码的函数是 umask. 通常使用的方法是 umask(0).

第五步:关闭文件描述符

1> 因为用 fork 函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。

2> 在上面的第二步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能到达守护进程,守护进程中常规方法(如 printf )输出的字符也不可能在终端上显示出来。所以,文件描述符为 0 , 1 和 2 的 3 个文件(常说的输入,输出和报错)已经失去了意义,也应该关掉。

3>函数实例:

for(i=0;i<MAXFILE;i++)

close(i);

第六步:处理 SIGCHLD 信号

1>处理 SIGCHLD 信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程( zombie) 从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务进程的并发性能。

2>函数实现:

signal(SIGCHLD,SIG_IGN);

这样,内核在子进程结束时不会产生僵尸进程。

6具体函数实现:

编写一个守护进程要包括两部分:主程序 test.c 和初始化程序 init.c 。

初始化程序中的 init_daemon 函数负责生成守护进程。利用 init_daemon 函数可以生成自己的守护进程。

daemon.c

1 #include<stdio.h>

2 #include<signal.h>

3 #include<sys/param.h>

4 #include<sys/types.h>

5 #include<sys/stat.h>

6 #include<stdlib.h>

7

8 int init_daemon(void)

9 {

10 pid_t pid;

11 int i;

12

13 pid = fork();

14 if(pid > 0){ //第一步,结束父进程,使得子进程成为后台

15 exit(0);

16 }

17 else if(pid < 0){

18 return -1;

19 }

20 /*第二步建立一个新的进程组,在这个新的进程组中,子进程成为这个进程组的首进程,以使该进程脱离所用终端*/

21 setsid();

22 /*再次新建一个子进程,退出父进程,保证该进程不是进程组长,同时让该进程无法再打开一个新的终端*/

23 pid = fork();

24 if(pid > 0){

25 exit(0);

26 }

27 else if(pid < 0){

28 return -1;

29 }

30 //第三步:关闭所用从父进程继承的不再需要的文件描述符

31 for(i = 0;i < NOFILE;close(i++));

32 //第四步:改变工作目录,使得进程不与任何文件系统联系

33 chdir("/");

34 //第五步:将文件屏蔽字设置为0

35 umask(0);

36 //第六步:忽略SIGCHLD信号

37 signal(SIGCHLD,SIG_IGN);

38 return 0;

39 }

test.c

1 #include<stdio.h>

2 #include<time.h>

3 #include<syslog.h>

4 extern int init_daemon(void);

5

6 int main()

7 {

8 time_t now;

9 init_daemon();//初始化Daemon

10 syslog(LOG_USER | LOG_INFO,"测试守护进程!/n");

11 while(1){

12 sleep(8);//睡眠一分钟

13 time(&now);

14 syslog(LOG_USER | LOG_INFO,"系统时间:/t%s/t/t/n",ctime(&now ));

15 }

16 }

17

程序在 ubuntu 2 .6 版本上进过调试

think@ubuntu:/etc$ gcc -g -o test daemon.c test.c

think@ubuntu:/etc$ ./test

编译成功后可以用 ps -ef 查看进程状态,

think@ubuntu:/etc$ ps -ef

UID PID   PPID C   STIME   TTY TIME   CMD

think 2995 1    0   11:05    ? 00:00:00   ./test

从此处可以看出该进程具备守护进程的所用特征

查看系统日志

think@ubuntu:~$ cat /var/log/syslog

Nov 13 11:05:37 ubuntu test: 测试守护进程!

Nov 13 11:05:45 ubuntu test: 系统时间: #011Sat Nov 13 11:05:45 2010#012#011#011

Nov 13 11:05:53 ubuntu test: 系统时间: #011Sat Nov 13 11:05:53 2010#012#011#011

Nov 13 11:06:01 ubuntu test: 系统时间: #011Sat Nov 13 11:06:01 2010#012#011#011

Nov 13 11:06:09 ubuntu test: 系统时间: #011Sat Nov 13 11:06:09 2010#012#011#011

Nov 13 11:06:17 ubuntu test: 系统时间: #011Sat Nov 13 11:06:17 2010#012#011#011

Nov 13 11:06:25 ubuntu test: 系统时间: #011Sat Nov 13 11:06:25 2010#012#011

exit()和_exit()函数

进程就好比人一样有其生命,我们通过fork()函数来创建一个进程,那么我们又是如何来中止进程呢。

进程退出

1.在Linux中任何让一个进程退出

进程退出表示进程即将结束。在Linux中进程退出分为了正常退出和异常退出两种。

1>正常退出

a. 在main()函数中执行return 。

b.调用exit()函数

c.调用_exit()函数

2>异常退出

a.调用about函数

b.进程收到某个信号,而该信号使程序终止。

Tiger-John说明:

不管是那种退出方式,系统最终都会执行内核中的同一代码。这段代码用来关闭进程所用已打开的文件描述符,释放它所占用的内存和其他资源。

3>比较以上几种退出方式的不同点

(1)exit和return 的区别:

a.exit是一个函数,有参数。exit执行完后把控制权交给系统

b.return是函数执行完后的返回。renturn执行完后把控制权交给调用函数。

(2)exit和abort的区别:

a.exit是正常终止进程

b.about是异常终止。

现在我们重点了解exit()和_exit()函数

2.exit()和_exit()的学习

1>exit和_exit函数都是用来终止进程的。

当程序执行到exit或_exit时,系统无条件的停止剩下所有操作,清除包括PCB在内的各种数据结构,并终止本进程的运行。

2>exit在头文件stdlib.h中声明,而_exit()声明在头文件unistd.h中声明。exit中的参数exit_code为0代表进程正常终止,若为其他值表示程序执行过程中有错误发生。

3>exit()和_exit()的区别:

a._exit()执行后立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。

b.调用_exit函数时,其会关闭进程所有的文件描述符,清理内存以及其他一些内核清理函数,但不会刷新流(stdin, stdout, stderr ...). exit函数是在_exit函数之上的一个封装,其会调用_exit,并在调用之前先刷新流。

Tiger-John说明:

exit()函数与_exit()函数最大区别就在于exit()函数在调用exit系统之前要检查文件的打开情况,把文件缓冲区的内容写回文件。由于Linux的标准函数库中,有一种被称作“缓冲I/O”的操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续的读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区读取;同样,每次写文件的时候也仅仅是写入内存的缓冲区,等满足了一定的条件(如达到了一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。这种技术大大增加了文件读写的速度,但也给编程代来了一点儿麻烦。比如有一些数据,认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时用_exit()函数直接将进程关闭,缓冲区的数据就会丢失。因此,要想保证数据的完整性,就一定要使用exit()函数。

c . 通过一个函数实例来看看它们之间的区别:

函数实例1 : exit.c

1 #include<stdio.h>

2 #include<stdlib.h>

3

4 int main()

5 {

6 printf("using exit----/n");

7 printf("This is the content in buffer/n");

8 exit(0);

9 }

函数经过调试后

think@ubuntu:~/work/process_thread/exit$ gcc exit.c -o exit

think@ubuntu:~/work/process_thread/exit$ ./exit

执行结果为:

using exit----

This is the content in buffer

函数实例2:_exit.c

1 #include<stdio.h>

2 #include<unistd.h>

3

4 int main(void)

5 {

6 printf("using _exit--/n");

7 printf("This is the content in buffer");

8 _exit(0);

9 }

函数经过调试后

think@ubuntu:~/work/process_thread/exit$ gcc _exit.c -o _exit

think@ubuntu:~/work/process_thread/exit$ ./_exit

执行结果为:

using _exit--

Tiger-John说明:

1.printf函数就是使用缓冲I/O的方式,该函数在遇到“/n”换行符时自动的从缓冲区中将记录读出。所以exit()将缓冲区的数据写完后才退出,而_exit()函数直接退出。

2.大家也可以把函数实例2中的printf("This is the content in buffer");改为printf("This is the content in buffer/n")(即在printf中最后加一个/n看运行结果是什么,为什么会产生这样的结果呢?)

Tiger-John补充:

父子进程终止的先后顺序不同会产生不同的结果。

1>父进程先于子进程终止:

此种情况就是我们前面所用的孤儿进程。当父进程先退出时,系统会让init进程接管子进程。

2>子进程先于父进程终止,而父进程又没有调用wait函数

此种情况子进程进入僵死状态,并且会一直保持下去直到系统重启。子进程处于僵死状态时,内核只保存进程的一些必要信息以备父进程所需。此时子进程始终占有着资源,同时也减少了系统可以创建的最大进程数。

什么是僵死状态呢?

一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息,释放它仍占有的资源)的进程被称为僵死进程(zombie)。ps命令将僵死进程的状态打印为Z

3>子进程先于父进程终止,而父进程调用了wait函数

此时父进程会等待子进程结束。

等待进程结束wait()和waitpid()函数
上一节最后我们说到若子进程先于父进程结束时,父进程调用wait()函数和不调用wait()函数会产生两种不同的结果:

--如果父进程没有调用wait()和waitpid()函数,子进程就会进入僵死状态。

--如果父进程调用了wait()和waitpid()函数,就不会使子进程变为僵尸进程。

这是为什么呢?现在我们来深入学习wait()函数和waitpid()函数。

一.wait()和waitpid()学习

1.首先我们先看一下它们的函数原型:

在终端输入命令:man 2 wait

就会看到它的函数原型:

NAME

wait, waitpid, waitid - wait for process to change state

SYNOPSIS

#include <sys/types.h>

#include <sys/wait.h>

pid_t wait(int *status);

pid_t waitpid(pid_t pid, int *status, int options);

int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

我们可以看到在2.6版本中新增叫了waitid()函数。

2.wait()和waitpid()的功能:

1>wait()函数使父进程暫停执行,直到它的一个子进程结束为止,该函数的返回值是终止运行的子进程的PID. 参数status所指向的变量存放子进程的退出码,即从子进程的main函数返回的值或子进程中exit()函数的参数。如果status不是一个空指针,状态信息将被写入它指向的变

量。

2> 头文件sys/wait.h中定义了进程退出状态的宏。

我们首先看下官方的解释

a.WIFEXITED(status) r eturns true if the child terminatednormally, that is, by calling exit(3) or _exit(2), or by returning from main().

Tiger-John翻译:

WIFEXITED(status) 若子进程是正常结束时则返回一个非零值。即调用exit(3),_exit(3) 或从main()函数返回的值。

b. WEXITSTATUS(status) returns the exit status of the child. This consists of theleast significant 8 bits of the status argument that the childspecified in a call to exit(3) or _exit(2) or as the argument for a return statement in
main(). This macro should only be employed if WIFEXITED returned true.

Tiger-John翻译:

WEXITSTATUS(status) 如果宏 WIFEXIED返回值为非零值时,它返回子进程中exit或_exit参数中的低8位。

c.W IFSIGNALED(status) returns true if the child process wasterminated by a signal.

Tiger-John翻译:

WIFSIGNALED(status) 若子进程异常终止则返回一个非零值。

d. WTERMSIG(status) returns the number of the signal that causedthe child process to terminate. This macro shouldonly be employed if WIFSIGNALED returned true.

Tiger-John翻译:

WTERMSIG(status) 如果宏WIFSIGNALED的返回值非零,则返回使子进程异常终止的信号编号。

e.WIFSTOPPED(status) returns true if the child process was stoppedby delivery of a signal; this is only possible if the call wasdone using WUN‐TRACED or when the child is being traced (see ptrace(2)).

Tiger-John翻译:

WIFSTOPPED(status) 若子进程由于异常暫停,则返回一个非零值。当调用 WUN‐TRACED或子进程被跟踪时这才时可能的。

f. WSTOPSIG(status) returns the number of the signal whichcaused the child to stop.This macro should only be employed if WIFSTOPPEDreturned true.

Tiger-John翻译:

WSTOPSIG(status) 如果宏WIFSTOPPED返回值非零,则返回使子进程暫停的信号编号。

g.WIFCONTINUED(status) (since Linux 2.6.10) returns true if the child process was

resumed by delivery of SIGCONT.

Tiger-John翻译:

WIFCONTINUED(status) (从2.6版本后)如果孩子进程通过SIGCONT恢复则返回一个非零

值。

3>waitpid() 函数

(1)我们先来看一个waitpid()的经典例子:当我们下载了A软件的安装程序后,在安装快结束时它又启动了另外一个流氓软件安装程序B,当B也安装结束后,才告诉你所有安装都完成了。A和B分别在不同的进程中,A如何启动B并知道B安装完成了呢?可以很简单地在A中用fork启动B,然后用 waitpid()来等待B的结束。

(2)waitpid()也用来等待子进程的结束,但它用于等待某个特定进程结束。参数pid指明要等待的子进程的PID,参数status的含义与wait()函数中的 status相同。options参数可以用来改变waitpid的行为,若将该参数赋值为WNOHANG,则使父进程不被挂起而立即返回执行其后的代

码。

(3)waitpid()函数中参数pid的取值

还是先看下官方解释:

Thevalue of pid can be:

< -1 meaning wait for any child process whose process group ID is

equal to the absolute value ofpid.

-1 meaning wait for any childprocess.

0 meaning wait for any childprocess whose process group ID is

equal to that of the callingprocess.

> 0 meaning wait for the child whose process ID is equal to the

value of pid.

Tiger-John翻译:

pid的值可以为下己中情况:

   < -1 等待其组ID等于pid绝对值的任一子进程。

  =-1 等待任一子进程

  =0等待其组ID等于调用进程的组ID的任一进程

   > 0 等待其进程ID等于pid的子进程退出

(4)waitpid()函数的一个应用:

 如果想让父进程周期性地检查某个特定的子进程是否已经退出,可以用下面的方法:

  waitpid(child_pid,(int *) 0,WNOHANG);

如果子进程尚未退出,它将返回0;如果子进程已经结束,则返回child_pid。调用失败时返回-1。失败的原因包括没有该子进程,参数不合法等。

3.wait()和 waitpid() 函数的区别

(1). 在一个子进程终止前,wait()使其调用者阻塞,而waitpid()有一个选项,可使调用者不阻塞。

(2). waitpid()并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。

(3). 对于wait(),其唯一的出错是调用进程没有子进程;对于waitpid(),若指定的进程或进程组不存在,或者参数pid指定的进程不是调用进程的子进程都可能出错。

(4). waitpid()提供了wait()没有的三个功能:一是waitpid()可等待一个特定的进程;二是waitpid()提供了一个wait()的非阻塞版本(有时希望取的一个子进程的状态,但不想使父进程阻塞,waitpid() 提供了一个这样的选择:WNOHANG,它可以使调用者不阻塞);三是waitpid()支持作业控制。

(5) wait(&status) 的功能就等于waitpid(-1, &status, 0);

函数实例:有时希望取的一个子进程的状态,但不想使父进程阻塞,waitpid() 提供了一个这样的选择:WNOHANG,它可以使调用者不阻塞

/* 如果是父进程 */

do{

pr=waitpid(pc, NULL, WNOHANG);

/* 使用了WNOHANG参数,waitpid不会在这里等待 */

if(pr==0){ /* 如果没有收集到子进程 */

rintf("No child exited/n");

sleep(1);

}

}while(pr==0); /* 没有收集到子进程,就回去继续尝试 */

if(pr==pc)

printf("successfully get child %d/n", pr);

else

printf("some error occured/n");

Tiger-John总结

无论进程是否正常终止,内核都会向其父进程发送SIGCHLD 信号, 当调用wait或waitpid函数时

(a) 如果所有的子进程都在run, 可以阻塞父进程。

(b) 如果子进程终止,则wait立即返回子进程终止状态。

(c) 如果没有子进程在运行, 立即返回error。

4.函数实现:

函数实例1.(先看一个简单的实例,看看进程调用wait()函数后是如何执行的?)

1#include<stdio.h>

2#include<sys/types.h>

3#include<sys/wait.h>

4#include<unistd.h>

5#include<stdlib.h>

6

7int main()

8 {

9 pid_t child;

10 int i;

11 child = fork();

12 if(child < 0){

13 printf("createfailed!/n");

14 exit(1);

15 }

16 else if (0 == child){

17 printf("this is the childprocess pid= %d/n",getpid());

18 for(i = 0;i<5;i++){

19 printf("this isthe child process print %d !/n",i+1);

20 }

21 printf("the child end/n");

22 }

23 else{

24 printf("this is the fatherprocess,ppid=%d/n",getppid());

25 printf("father wait thechild end/n");

26 wait(&child);

27 printf("fatherend/n");

28 }

29

30

31 }

函数经过编译:

think@ubuntu:~/work/process_thread/wait$ gccwait.c -o wait

think@ubuntu:~/work/process_thread/wait$./wait

函数执行结果:

this is the father process,ppid=3303

father wait the child end

this is the child process pid= 3356

this is the child process print 1 !

this is the child process print 2 !

this is the child process print 3 !

this is the child process print 4 !

this is the child process print 5 !

the child end

father end

Tiger-John说明:

从上面的程序我们可以深入的了解wait() 函数的执行过程:

当父进程调用wait()函数后被挂起等待,直到子进程结束为止。

函数实例2(现在我们在通过一个实例,来深入了解wait()函数的执行过程)

1#include<stdio.h>

2#include<sys/types.h>

3#include<sys/wait.h>

4#include<unistd.h>

5#include<stdlib.h>

6int main()

7 {

8 pid_t pid;

9 char *msg;

10 int i;

11 int exit_code;

12

13 printf("tiger study how to get exit code/n");

14 pid = fork();

15 if(0 == pid){

16 msg = " child process isrunning";

17 i = 5;

18 exit_code = 37;

19 }

20 else if(pid >0){

21 exit_code = 0;

22 }

23 else{

24 perror("process creationfailed/n");

25 exit(1);

26 }

27 if(pid > 0){

28

29 int status;

30 pid_t child_pid;

31

32 child_pid = wait(&status);

33

34 printf("child process hasexited,pid = %d/n",child_pid);

35 if(WIFEXITED(status)){

36 printf("childexited with code %d/n",WEXITSTATUS(status));

37 }

38 else{

39 printf("childexited abnormally/n");

40 }

41 }

42 else{

43 while(i-- > 0){

44 puts(msg);

45 sleep(1);

46 }

47 }

48 }

函数进过编译后:

think@ubuntu:~/work/process_thread/wait$gcc wait1.c -o wait1

think@ubuntu:~/work/process_thread/wait$./wait1

函数执行结果:

tiger study how to get exit code

child process is running

child process is running

child process is running

child process is running

child process is running

child process has exited,pid = 3816

child exited with code 0

Tiger-John说明:

父进程调用wait()函数后被挂起(我们可以再开一个终端,输入命令:ps aux,可以看到父进程的执行结果为S)直到子进程结束。子进程结束后,wait()函数返回刚刚结束运行的子进程的pid,宏WEXITSTATUS获取子进程的退出码。

进程控制函数之exec()函数的学习

当我们看恐怖片时,经常会有这样的场景:当一个人被鬼上身后,这个人的身体表面上还和以前一样,但是他的灵魂和思想已经被这个鬼占有了,因此它会控制这个人做他自己想做的事情--那么在进程中也有这样的情景。那么是如何实现的呢?现在我们来学习exec()函数族

一.exec()函数族

1. 首先我们在终端输入命令:man exec 可以看到函数的原型:

#include <unistd.h>

int execl(const char *path, const char *arg, ...);

int execv(const char *path, char *const argv[]);

int execle(const char *path, const char *arg,..., char * const envp[]);

int execve(const char*pathname, const char *argv[], char *const envp[]);

int execlp(const char *file, const char *arg, ...);

int execvp(const char *file, char *const argv[]);

这些函数之间的第一个区别是前4个取路径名做参数,后两个则取文件名做参数。

当指定filename做参数时:

a. 如果filename中包含/,则将其视为路径名

b. 否则就按PATH环境变量搜索可执行文件。

PATH=/bin:/usr/bin:/user/local/bin:.

最后的路径前缀表示当前目录。零前缀耶表示当前目录(在name=value开始处可用:表示,在中间用::表示,在行尾用:表示)

第二个区别与参数表的传递有关(l表示list,v表示矢量vector)。函数execl、execlp和execle要求将新程序的每个命令行参数都说明为一个单独的参数,这中参数表以空指针结尾。而execv、execve和execvp则要先构造一个指向各参数的指针数组,然后将该数组的地址作为这三个函数的地址。

第三个区别与向新程序传递环境表相关。函数execve和execle可以传递一个指向环境字符串指针数组的指针。

Tiger-John总结:1>.其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。

2>.exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。

但是若为Shell脚本时必须遵守以下的格式开头:

第一行必须为: #!interpretername[arg]。其中interpretername可以时 shell或其他解释器。例如,/bin/sh或/usr/bin/perl,arg是传递个解释器的参数。

3>记忆方法:

l : 表示使用参数列表(list)

e:表示使用新的环境变量,不从当前继承

p: 表示使用文件名,并从PATH环境进行搜索

4> exec()函数族成功后是不会返回值的,因为进程的执行映像已经被替换,没有接收返回值的地方了。但是若有一个错误的事件,将会返回-1.这些错误通常是有文件名或参数错误引起的。

2.exec()函数的功能:

1>exec()函数调用并没有生成新进程,一个进程一旦调用exec函数,它本省就“死亡了”--就好比被鬼上身一样,身体还是你的,但灵魂和思想已经被替换了--系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一保留的就是进程ID。也就是说,对系统而言,还是同一个进程,不过执行的已经是另外一个程序了。

2>执行exec()函数后的进程除了保持了原来的进程ID,父进程ID,实际用户ID和实际组ID之外,进程还保持了其他许多原有特征,主要有

a.当前工作目录

b.根目录

c.创建文件时使用的屏蔽字

d.进程信号屏蔽字。

e. 未决警告

f.和进程相关的使用处理器的时间

g.控制终端

h.文件锁

Tiger-John说明:

1.此处要分清只有fork()或vfork()函数才能创建一个新进程,而exec()函数时不能创建进程的。

2.因此在使用exec()函数之前,先要使用fork()或vfork()创建子进程后,子进程调用exec()函数来执行另外一个程序。

3.exec()函数族的具体实现

1>当进程调用一种exec()函数时,该进程执行的程序完全替换为新程序,而新程序则从main函数开始执行。因为调用exec()并不创建新进程,所以前后的进程ID不变。函数exec()只是用一个全新的程序替换当前进程的正文、数据、堆和栈段。

2>无论是哪个exec()函数,都是将可执行程序的路径,命令行参数和环境变量3个参数传递个可执行程序的main()函数。

3>具体介绍exec()函数族是如何main()函数需要的参数传递个它的。

a.execv()函数:execv()函数是通过路径名方式调用可执行文件作为新的进程映像。它的argv参数用来提供给main()函数的argv参数。argv参数是一个以NULL结尾的字符串数组

b.execve()函数:参数pathname时将要执行的程序的路径名,参数argv,envp 与main()函数的argv,envp对应 。

c.execl()函数:次函数与execv函数用法类似。只是在传递argv 参数的时候,每个命令行参数都声明为一个单独的参数(参数中使用“......"说明参数的个数是不确定的),要注意的是这些参数要以一个空指针作为结束。

d.execle()函数:该函数与execl函数用法类似,只是要显示指定环境变量。环境变量位于命令行参数最后一个参数的后面,也就是位于空指针之后。

e.execvp函数:该函数和execv函数用法类似,不同的是参数filename。该参数如果包含/,则将其视为路径名,否则就按PATH环境变量搜索可执行文件。

f.execlp()函数:该函数于execl函数类似,它们的区别和execvp与execv的区别一样。

----------------------------------------------

通过以上学习,我们来编个程序来体验下它的执行过程

4.函数实例

exec.c

1 #include<stdio.h>

2 #include<sys/types.h>

3 #include<unistd.h>

4 #include<stdlib.h>

5 int main(int argc,char *argv[],char ** environ)

6 {

7 pid_t pid;

8 int status;

9 printf("Exec example!/n");

10 pid = fork();

11 if(pid < 0){

12 perror("Process creation failed/n");

13 exit(1);

14 }

15 else if(0 == pid){

16 printf("child process is running/n");

17 printf("My pid = %d ,parentpid = %d/n",getpid(),getpid());

18 printf("uid = %d,gid = %d/n",getuid(),getgid());

19 execve("processimage",argv,environ);

20 printf("process never go to here!/n");

21 exit(0);

22 }

23 else {

24 printf("Parent process is runnig/n");

25 }

26 wait(&status);

27 exit(0);

28 }

processimage.c

1 #include<stdio.h>

2 #include<sys/types.h>

3 #include<unistd.h>

4

5 int main(int argc,char * argv[],char ** environ)

6 {

7 int i;

8

9 printf("I am a process image!/n");

10 printf("My pid =%d,parentpid = %d/n",getpid(),getppid());

11 printf("uid = %d,gid = %d/n",getuid(),getpid());

12

13 for(i = 0;i<argc;i++){

14 printf("argv[%d]:%s/n",i,argv[i]);

15 }

16

17 }

函数经过编译:

think@ubuntu:~/work/process_thread/exec1$ gcc processimage.c -o processimage

think@ubuntu:~/work/process_thread/exec1$ gcc exec.c -o exec

think@ubuntu:~/work/process_thread/exec1$ ./exec test exec

函数执行结果:

Exec example!

Parent process is runnig

child process is running

My pid = 5949 ,parentpid = 5949

uid = 1000,gid = 1000

I am a process image!

My pid =5949,parentpid = 5948

uid = 1000,gid = 5949

argv[0]:./exec

argv[1]:test

argv[2]:exec

Tiger-John说明:

1.通过上面程序的执行,我们可以看到执行新程序的进程保持了原来进程的进程ID,父进程ID,实际用户ID和实际组ID。

2. 当调用execve()函数后,原来的子进程的映像被替代,不再执行。

转自:http://blog.csdn.net/tigerjibo/article/details/6008589
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: