Linux的多任务编程-进程
进程是指一个具有独立功能的程序在某个数据集合上的一次动态执行过程,它是系统进行资源分配和调度的基本单元.一次任务的运行可以并发激活多个进程,这些进程相互合作来完成该任务的一个最终目标.
进程的特性:并发性,动态性,交互性,独立性,异步性.
进程的种类:交互式进程,批处理进程,实时进程.
进程和程序是有本质区别的:程序是静态的一段代码,是一些保存在非易失性存储器的指令的有序集合,没有任何执行的概念;而进程是一个动态的概念,它是程序执行的过程,包括了动态创建,调度和消亡的整个过程,它是程序执行和资源管理的最小单位.
进程状态:运行状态,可中断的阻塞状态,不可中断的阻塞状态,可终止的阻塞状态,暂停状态,跟踪状态,僵尸状态,僵尸撤销状态.
进程状态转换关系:
进程是构成Linux系统应用的一块基石,它代表了一个Linux系统上的绝大部分活动,不管你是系统程序员,应用程序员,还是系统管理员,弄明白Linux的进程管理将使你"一切尽在掌握".
一个正在运行的程序(或者叫进程),是由程序代码,数据,变量(占用着系统内存),打开的文件(文件描述符)和一个环境组成.通常,Linux系统会让进程共享代码和系统库,所以在任何时刻内存里都只有代码的一份拷贝.例如,不管有多少进程在调用printf()函数,内存里只需要有一份它的代码就够了.
每个进程都会分配到一个独一无二的数字编号,我们称之为"进程标识码"(Process identifier,PID),它这是一个正整数,取值范围从2到32768.当一个进程被启动的时候,它会分配到一个未使用的编号数字做为自己的PID.虽然该编号是唯一的,但是当一个进程终止后,其PID就可以再次使用了.根据系统具体实现的不同,大多数的系统则会将所有可有的PID轮过一圈后,再考虑使用之前释放出的PID.
Linux内核通过惟一的进程标识符PID来标识每个进程.PID存放在进程描述符的pid字段中.在Linux中获得当前进程的进程号(PID)和父进程号(PPID)的系统调用函数分别为getpid()和getppid().
表示进程的数据结构是struct task_struct.task_struct结构是进程实体的核心,Linux内核通过对该结构的相关操作来控制进程,task_struct结构是一个进程存在的唯一标志,也就是通常说的进程控制块(PCB,Process Control Block).
Linux将所有task_struct结构的指针存储在task数组中,数组的大小就是系统能容纳的进程数目,默认为512.
与其他的操作系统有所不同,为了实现创建进程的开销尽可能低,在Linux中"创建一个新的进程"与"在一个进程中运行一个给定的操作"是有所区别的.不过这样的区别在概念上并不十分重要,而是通过这样的观点设计出的Linux内核具有了很好的多进程性能,这样的设计思想是值得我们去学习的.一个现有的进程可以调用fork()函数创建一个新的进程.
fork()函数用于从已存在的进程中创建一个新进程.新进程称为子进程,而原进程称为父进程.使用fork()函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程上下文、代码段、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等,而子进程所独有的只有它的进程号、资源使用和计时器等.fork函数的原型和返回值如下:
注:unistd.h 是 C 和 C++ 程序设计语言中提供对 POSIX 操作系统 API 的访问功能的头文件的名称.该头文件由 POSIX.1 标准(单一UNIX规范的基础)提出,故所有遵循该标准的操作系统和编译器均应提供该头文件(如 Unix 的所有官方版本,包括 Mac OS X,Linux 等).对于类 Unix 系统,unistd.h 中所定义的接口通常都是大量针对系统调用的封装,如 fork,pipe 以及各种 I/O 原语(read,write,close 等等).
fork()函数的使用很简单,下面通过一个简单的例子来进一步学习.
int main() { pid_t pid; char *message; int n = 6;/* 全局变量 */ printf("fork program starting\n"); pid = fork(); switch(pid) { case -1: perror("fork failed"); exit(1); case 0: message = "This is the child"; n ++; break; default: message = "This is the parent"; n --; break; } printf("%s: pid = %d, n=%d\n",message,getpid(),n); exit(0); }
#include <sys/types.h> #include <unistd.h> #include <stdio.h> int main() { pid_t pid; char *message ; int n = 6;/* 全局变量 */ /* 输出重定向 */ message = “Message From STDOUT\n”; if (write(STDOUT_FILENO, message ,sizeof(message)-1) != sizeof(message)-1) perror(“write error”) printf("fork program starting\n"); pid = fork(); switch(pid) { case -1: perror("fork failed"); exit(1); case 0: message = "This is the child"; n ++; break; default: message = "This is the parent"; n --; sleep(2); break; } printf("%s: pid = %d, n=%d\n",message,getpid(),n); exit(0); }
#include <unistd.h> int execl (const char *pathname, const char *arg0, …); int execv (const char *pathname, char *const argv[]); int execle (const char *pathname, const char *arg0, …, char *const envp[]); int execve (const char *pathname, char *const argv[], char *const envp[]); int execlp (const char *filename, const char *argv0,…); int execvp (const char *filename, char *const argv[]);
#include <unistd.h> #include <stdio.h> int main() { printf("Start exec() \n"); execlp("ps", "ps", "-ax", 0); printf("Done.\n"); exit(0); }
exec函数族使用区别:
查找方式
#include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { printf("Start exec() \n"); execlp("ps", "-ax", (char *)0); printf("Done.\n"); exit(0); }函数的运行结果如下:
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { pid_t pid; const char *usr_envp[ ] = {"MYDEFINE=unknown","PATH=/tmp", (char *)0}; printf ("Begin fork()\n"); pid = fork(); switch(pid) { case -1: perror("fork failed"); exit(1); case 0: if (execle("/tmp/child","myarg1","my arg2", (char *)0, usr_envp)<0) perror("execle failed"); break; default: break; } if (waitpid (pid, NULL, 0) < 0) perror("waitpid failed"); printf ("parent exiting\n"); exit(0); }在该程序中的父进程中首先创建了一个新进程,然后在子进程中调用execle函数,并将命令行参数和环境变量字符都传给了新进程.子进程的功能就是打印出所有的命令行参数和所有的环境变量字符串.子进程的代码如下:
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main ( int argc , char *argv[ ] ,char *envp[]) { int i; char **ptr; printf ("child starting\n"); for ( i = 0; i < argc; i++) printf ("argv[%d] : %s\n",i, argv[i]); for ( ptr = envp; *ptr != 0 ; ptr++) printf ("%s\n",*ptr); printf ("child exiting\n"); exit(0); }下图是该程序的运行结果:
int main (int argc, char *argv[ ])其中,argc是命令行参数的数目,argv是指向参数的各个指针所构成的数组.当内核执行C程序时,即使用exec()函数执行一个程序,内核首先开启一个特殊的启动例程,该例程从内核取得命令行参数和环境变量值,然后调用main()函数.
- 由main()函数返回;
- 调用exit()函数;
- 调用_exit()或_Exit()函数.
由main函数返回的程序,一般会在函数的结尾处通过return语句指明函数的返回值,如果不指定这个返回值,main函数通常会返回0.但这种特性与编译器有关,因此为了程序的的通用性,应该主动养成使用return语句的习惯.下面是一个使用exit函数的例子.#include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { pid_t pid; char *message ; int exit_code ; printf ("Begin fork()\n"); pid = fork(); switch(pid) { case -1: perror("fork failed"); exit(1); case 0: message = "This is the child"; exit_code = 37; break; default: message = "This is the parent"; exit_code = 0; break; } printf("%s: pid = %d\n",message,getpid()); if(pid) { int stat_val; pid_t child_pid; child_pid = wait(&stat_val); printf("Child has finished: PID = %d\n", child_pid); if(WIFEXITED(stat_val)) printf("Child exited with code %d\n", WEXITSTATUS(stat_val)); else printf("Child terminated abnormally\n"); } exit (exit_code); }
可以看出,在主进程中得到了子进程的退出状态值37.获得该值的方法将在后面详细讲解.
- 当进程接收到某些信号时;或是调用abort()函数,它产生SIGABRT信号.这是前一种的特例.
这便是进程异常终止的两种方式。一个进程正常退出后传递了一个退出状态给系统,如return语句和exit()等函数.退出值是一个8位值,通常为一个int型的值.通常退出状态0表示正常退出,任何非0的退出状态表示出现了某种错误.
exit()和_exit()
exit()和_exit()函数都是用来终止进程的.当程序执行到exit()或_exit()时,进程会无条件地停止剩下的所有操作,清除包括各种数据结构,并终止本进程的运行.
exit()和_exit()的区别
前面我们已经多次用到了wait()和waitpid(),这两个函数的原型是:
wait()函数是用于使父进程(也就是调用wait()的进程)阻塞,直到一个子进程结束或者该进程接到了一个指定的信号为止.如果该父进程没有子进程或者他的子进程已经结束,则wait()就会立即返回。
waitpid()的作用和wait()一样,但它并不一定要等待第一个终止的子进程,它还有若干选项,如可提供一个非阻塞版本的wait()功能,也能支持作业控制
下面有几个宏可判别结束情况:
WIFEXITED(status)如果子进程正常结束则为非0 值.
WEXITSTATUS(status)取得子进程exit()返回的结束代码,一般会先用WIFEXITED来判断是否正常结束才能使用此宏.
WIFSIGNALED(status)如果子进程是因为信号而结束则此宏值为真.
WTERMSIG(status) 取得子进程因信号而中止的信号代码,一般会先用WIFSIGNALED来判断后才使用此宏.
WIFSTOPPED(status) 如果子进程处于暂停执行情况则此宏值为真.一般只有使用WUNTRACED时才会有此情况.
WSTOPSIG(status) 取得引发子进程暂停的信号代码,一般会先用WIFSTOPPED来判断后才使用此宏.
waitpid函数可以提供wait函数所没有的三个特性:
#include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <stdlib.h> int main(int argc, char *argv[]) { pid_t pid, w; int status; char *message ; printf ("Begin fork()\n"); pid = fork(); switch(pid) { case -1: perror("fork failed"); exit(EXIT_FAILURE); case 0: message = "This is the child"; printf("%s: pid = %d\n",message,getpid()); if (argc == 1) pause(); _exit(atoi(argv[1])); break; default: message = "This is the parent"; break; } printf("%s: pid = %d\n",message,getpid()); do { w = waitpid(pid, &status, WUNTRACED | WCONTINUED); if (w == -1) { perror("waitpid"); exit(EXIT_FAILURE); } if (WIFEXITED(status)) { printf("exited, status=%d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("killed by signal %d\n", WTERMSIG(status)); } else if (WIFSTOPPED(status)) { printf("stopped by signal %d\n", WSTOPSIG(status)); } else if (WIFCONTINUED(status)) { printf("continued\n"); } } while (!WIFEXITED(status) && !WIFSIGNALED(status)); exit(EXIT_SUCCESS); }我们首先看看带参数运行的结果:
创建子进程是十分容易的,但你必须密切注意子进程的执行情况.当一个子进程结束运行的时候,它与其父进程之间的关联还会保持到父进程也正常地结束运行或者父进程调用了wait()才告终止.因此,进程表中代表子进程的数据项是不会立刻释放的,虽然不再活跃了,可子进程还停留在系统里,因为它的退出码还需要保存起来以备父进程中后续的wait()调用使用.它将成为一个Zombie 进程("僵尸进程").