一、 进程基本知识
1.1 进程概念
进程是程序执行和资源管理的最小单位,是程序动态执行的过程。
Linux下是通过进程控制块(PCB)来描述一个进程的,进程控制块包含了进程的描述信息、控制信息以及资源信息,它是进程的一个静态描述。在Linux中进程控制块中的每一项都是一个task_struct结构,是在include/linux/sched.h中定义的。
1.2 进程标识
在Linux中进程的主要标识有进程标识号(PID)以及父进程标识号(PPID)。PID唯一的标识一个进程。PPID、PID都是一个非零的正整数。获得当前进程的PID和PPID的系统调用分别为
1: #include <sys/types.h>
2: #include <unistd.h>
3: pid_t getpid(void);
4: pid_t getppid(void);
1.3 进程的各个状态
进程状态的宏定义
1: #define TASK_RUNNING 0
2: #define TASK_INTERRUPTIBLE 1
3: #define TASK_UNINTERRUPTIBLE 2
4: #define TASK_STOPPED 4
5: #define TASK_TRACED 8
6: /* in tsk->exit_state */
7: #define EXIT_ZOMBIE 16
8: #define EXIT_DEAD 32
9: /* in tsk->state again */
10: #define TASK_NONINTERACTIVE 64
11: #define TASK_DEAD 128
状态 |
状态名称 |
说明 |
TASK_RUNNING |
运行状态 |
表示进程正在被CPU执行,或者已经准备就绪随时可由调度程序调度执行。 |
TASK_INTERRUPTIBLE |
可中断睡眠状态 |
进程处于等待状态,不会被调度执行。直到等待的资源可用(或等待某条件为真)或者系统产生一个中断或进程收到一个信号时,进程就被唤醒继而进入就绪状态(TASK_RUNNING)。 |
TASK_UNINTERRUPTIBLE |
不可中断的睡眠状态 |
与TASK_INTERRUPTINLE状态的唯一区别就是该状态不可被收到的信号唤醒。 |
TASK_STOPPED |
暂停状态 |
当进程收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU信号后就会进入TASK_STOPPED状态。可向其发送SIGCONT信号让进程转换到可运行状态。 |
TASK_TRACED |
跟踪状态 |
进程的执行已由debugger程序暂停。当一个进程被另一个进程监控时(例如debugger执行ptrace()系统调用监控一个测试程序),任何信号都可以把这个进程置于TASK_TRACED状态。 |
TASK_NONINTERACTIVE |
不可交互睡眠状态 |
该状态必须和 TASK_INTERRUPTIBLE 或者 TASK_UNINTERRUPTIBLE状态组合使用。它在进程处于睡眠状态并且无论它是否可以交互都不提供任何信息的时候使用。 |
TASK_DEAD |
死亡状态 |
task_struct->state == EXIT_DEAD是一个特殊情况,为了避免混乱就引入了这个新的状态。EXIT_DEAD就只能用于->exit_state字段。一个进程在退出(调用do_exit())时,state字段都被置于TASK_DEAD状态。 |
EXIT_ZOMBIE |
僵死进程 |
该状态是task_struct->exit_state字段的值,表示进程的执行被终止,但是服进程还没有发布wait4()或waitpid()系统调用来返回有关死亡的进程信息。发布wait()类系统调用前,内核不能丢弃包含在死亡进程描述符中的数据,因为父进程可能还需要它来取得进程的退出状态。 |
EXIT_DEAD |
僵死撤销状态 |
该状态也是task_struct->exit字段的值,表示进程的最终状态。由于父进程刚发出wait4()或waitpid()系统调用,因而进程由系统删除,为了防止其他执行线程在同一个进程也执行wait()类系统调用,而把进程的状态由僵死状态(EXIT_ZOMBIE)改为撤销状态(EXIT_DEAD)。 |
1.4 进程结构
在Linux下每一个进程都运行在独立虚拟内存空间下,每个进程包含3个段:
数据段 |
存放全局变量、常数以及动态数据分配的数据空间。 |
代码段 |
存放程序代码的数据 |
堆栈段 |
存放的是子程序的返回地址、子程序的参数以及程序的局部变量。 |
二、 创建一个新进程
在linux下创建一个新进程的方法是调用fork()。fork()函数是进程控制中比较特殊的函数,它运行一次产生两个返回值。
2.1 fork()原型
1: #include <sys/types.h> /* 定义pid_t类型 */
2: #include <unistd.h>
3: pid_t fork(void);
返回值: 0: 子进程
-1: 错误
子进程PID: 父进程
2.2 fork()说明
fork()函数从已存在进程中创建一个新进程,新进程为该进程的子进程,原进程则为父进程。这个两个进程分别带回他们的返回值,父进程中返回的是子进程的PID,而子进程返回0。因此可以通过返回值来判断是子进程还是父进程。
使用fork()函数会得到父进程的一个复制进程,它从父进程处继承了整个进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。子进程独有的是自己的进程号、资源使用和计时器。所以使用fork()函数代价很大,它复制了父进程的代码段、数据段、堆栈段中的大部分内容,是的fork()函数的执行速度并不快。另一种高效率的进程创建函数为vfork()。
fork()将程序切成三段:
上图表示一个含有fork的程序,而fork语句可以看成将程序切为A、B两个部分。然后整个程序会如下运行:
step1、设由shell直接执行程序,生成了进程P。P执行完Part. A的所有代码。
step2、当执行到pid = fork();时,P启动一个进程Q,Q是P的子进程,和P是同一个程序的进程。Q继承P的所有变量、环境变量、程序计数器的当前值。
step3、在P进程中,fork()将Q的PID返回给变量pid,并继续执行Part. B的代码。
step4、在进程Q中,将0赋给pid,并继续执行Part. B的代码。
2.3 fork()使用范例
1: #include <sys/types.h> //define type -- pid_t
2: #include <unistd.h>
3: #include <stdio.h>
4: #include <stdlib.h>
5:
6: int main(void)
7: {
8: pid_t result;
9:
10: //create a subprocess
11: result = fork();
12:
13: if(result < 0) //result == -1 --- error accured
14: {
15: perror("creating subprocess");
16: exit(1);
17: }
18: else if(result == 0) //result == 0 --- current process is subprocess
19: {
20: printf("---------------\nresult = %d\nsubPID = %d\n", result, getpid());
21: }
22: else if(result > 0) //result > 0 --- current process is parent process
23: {
24: sleep(2);
25: printf("---------------\nresult = %d\nPPID = %d\n", result, getpid());
26: }
27:
28: return 0;
29: }
30:
三、 Replacing a process image
要想运行一个新进程,最基础要做到下面两点:
1.创建一个可运行程序的环境,也就是进程。
2.将环境中的内容替换成你所希望的,也就是用你希望运行的可执行文件去覆盖新进程中的原有映像,并从该可执行文件的起始处开始执行。
fork()可以完成第一点,但是由fork()创建的进程复制了父进程的大部分数据和状态,exec系列函数提供了设定新进程数据的方法,它可以根据制定的文件名或者目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完成之后,原调用进程的内容除了进程号以外,其他全部被新的进程替换了。(这里的可执行文件既可以是二进制文件,也可以是Linux下可执行的脚本。)
3.1 exec函数原型
1: 1: #include <unistd.h>
2: 2: char **environ;
3: 3: int execl(const char *path, const char *arg0, ..., (char *)0);
4: 4: int execlp(const char *file, const char *arg0, ..., (char *)0);
5: 5: int execle(const char *path, const char *arg0, ..., (char *)0, char *const envp[]);
6: 6: int execv(const char *path, char *const argv[]);
7: 7: int execvp(const char *file, char *const argv[]);
8: 8: int execve(const char *path, char *const argv[], char *const envp[]);
9: char *path: 可执行文件的完整路径。
10: char *file: 可执行文件的文件名,系统会自动从环境变量$PATH所指定的路径查找。
11: char *arg0: 可执行文件的参数,使用list方式列出,最后是NULL(即char * 0)。
12: char argv[]:可执行文件的参数队列,最后一个元素是NULL。
13: char *envp[]: 要指定的环境变量信息。
14: -1:出错。
3.2 函数名含义
前四位 |
统一为:exec |
|
第五位 |
l: 参数传递为逐个列举方式。arg0, …, NULL |
execl, execle, execlp |
v: 参数传递为构造指针数据方式。argv[] |
execv,execve, execvp |
|
第六位 |
e:可传递新进程环境变量。 envp[] |
execle,execve |
p:可执行文件查找方式为文件名。 file |
execlp, execvp |
3.3 exec函数使用范例
1: 1: #include <unistd.h>
2: 2: /* Example of an argument list */
3: 3: /* Note that we need a program name for argv[0] */
4: 4: char *const ps_argv[] = {“ps”, “ax”, 0};
5: 5: /* Example environment, not terribly useful */
6: 6: char *const ps_envp[] = {“PATH=/bin:/usr/bin”, “TERM=console”, 0};
7: 7: /* Possible calls to exec functions */
8: 8: execl(“/bin/ps”, “ps”, “ax”, 0); /* assumes ps is in /bin */
9: 9: execlp(“ps”, “ps”, “ax”, 0); /* assumes /bin is in PATH */
10: 10: execle(“/bin/ps”, “ps”, “ax”, 0, ps_envp); /* passes own environment */
11: 11: execv(“/bin/ps”, ps_argv);
12: 12: execvp(“ps”, ps_argv);
13: 13: execve(“/bin/ps”, ps_argv, ps_envp);