程序与进程:
程序(program)是一个普通文件,是机器代码指令和数据的集合,这些指令和数据存储在磁盘上的一个可执行映像中。所谓可执行映像就是一个可执行文件的内容。使用6个exec函数中的一个由内核将程序读入内存,并使其执行。
进程(process)是一个动态的实体,它具有生命周期,系统中进程的生死随时发生。程序的执行实例被称为进程。
专用进程:
进程ID0是调度进程,常常被称为交换进程(swapper)。该进程并不执行任何磁盘上的程序,它是内核的一部分,因此也被称为系统进程。
它是由完成内核初始化工作的start_kernel()函数创建,又叫闲逛进程(Idle Process)。进程0执行的是cpu_idle()函数,该函数中只有一条hlt汇编指令,hlt指令在系统闲置时不仅能降低电力的使用还能减少热的产生。例如,进程0的PCB叫做init_task,在很多链表中起链表头的作用,当就绪队列没有其它进程时,闲逛进程0就被调度程序选中,以此达到省电的目的。
进程ID1通常是init进程,在自举过程结束时由内核调用。init进程绝不会终止,因为它创建和监控操作系统外层所有进程的活动。它是一个普通的用户进程,但是它以超级用户特权运行。
init进程是Linux在启动时创建的特殊进程,顾名思义,它是起始进程,是祖先,以后诞生的所有进程都是它的后代——或是它儿子,或是它的孙子。init进程为每个终端(TTY)创建一个新的管理进程,这些进程在终端上等待用户的登录。当用户正确登录后,系统再为每一个用户启动一个shell进程,由shell进程等待并接收用户输入的命令信息。此外,init进程还负责管理系统中的“孤儿”进程。
进程ID2是页精灵进程(pagedaemon)。此进程负责支持虚存系统的请页操作。是一个内核进程。
进程控制:建立新进程,运行程序,终止进程。
问题1:一个程序如何运行另一个程序?
答案1:exec系统调用
一、通常子进程调用一种exec函数以执行另一程序,当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其main函数开始执行。因为调用exec函数并不创建新进程,所以前后的进程ID并未改变。exec只是用一个新程序替换了当前进程的正文、数据、堆和栈段。
实际上在Linux中,并不存在一个exec()的函数形式,exec指的是一组函数,一共有6个,分别是:
1 #include <unistd.h>
2 extern char **environ;
3 int execl(const char *path, const char *arg, ...);
4 int execlp(const char *file, const char *arg, ...);
5 int execle(const char *path, const char *arg, ..., char * const envp[]);
6 int execv(const char *path, char *const argv[]);
7 int execvp(const char *file, char *const argv[]);
8 int execve(const char *path, char *const argv[], char *const envp[]);/*execve第1个参数path是被执行应用程序的完整路径,第2个参数argv就是传给被执行应用程序的命令行参数,第3个参数envp是传给被执行应用程序的环境变量。argv数组和envp数组存放的都是指向字符串的指针,这两个数组都以一个NULL元素表示数组的结尾。*/
其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。
exec函数族的作用就是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行了一个可执行文件。其可执行文件既可以是二进制文件,也可以是linux下任何可执行的脚本文件。
与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。
分析:
参数argc指出了运行该程序时命令行参数的个数,数组argv存放了所有的命令行参数,数组envp存放了所有的环境变量。(环境变量指的是一组值,从用户登录后就一直存在,很多应用程序需要依靠它来确定系统的一些细节,我们最常见的环境变量是PATH,它指出了应到哪里去搜索应用程序,如 /bin;HOME也是比较常见的环境变量,它指出了我们在系统中的个人目录。环境变量一般以字符串"XXX=xxx"的形式存在,XXX表示变量名,xxx表示变量的值。)
留心看一下这6个函数还可以发现,前3个函数都是以execl开头的,后3个都是以execv开头的,它们的区别在于,execv开头的函数是以"char *argv[]"这样的形式传递命令行参数,而execl开头的函数采用了我们更容易习惯的方式,把参数一个一个列出来,然后以一个NULL表示结束。这里的NULL的作用和argv数组里的NULL作用是一样的。
在全部6个函数中,只有execle和execve使用了char *envp[]传递环境变量,其它的4个函数都没有这个参数,这并不意味着它们不传递环境变量,这4个函数将把默认的环境变量不做任何修改地传给被执行的应用程序。而execle和execve会用指定的环境变量去替代默认的那些。
还有2个以p结尾的函数execlp和execvp,咋看起来,它们和execl与execv的差别很小,事实也确是如此,除execlp和execvp之外的4个函数都要求,它们的第1个参数path必须是一个完整的路径,如"/bin/ls";而execlp和execvp的第1个参数file可以简单到仅仅是一个文件名,如"ls",这两个函数可以自动到环境变量PATH制定的目录里去寻找。
注:在以后的编程中遇到exec函数族,一定要记得加错误判断语句,因为与其他系统调用比起来,exec很容易受伤,被执行文件的位置,权限等很多因素都能导致该调用的失败。最常见的错误是:a、找不到文件或路径,此时errno被设置为ENOENT;b、数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT;c、没有对要执行文件的运行权限,此时errno被设置为EACCES。
exec()demo:
1 /** prompting shell version 2
2 **
3 ** Solves the `one-shot‘ problem of version 1
4 ** Uses execvp(), but fork()s first so that the
5 ** shell waits around to perform another command
6 ** New problem: shell catches signals. Run vi, press ^c.
7 **/
8
9 #include <stdio.h>
10 #include <signal.h>
11
12 #define MAXARGS 20 /* cmdline args */
13 #define ARGLEN 100 /* token length */
14
15 main()
16 {
17 char *arglist[MAXARGS+1]; /* an array of ptrs */
18 int numargs; /* index into array */
19 char argbuf[ARGLEN]; /* read stuff here */
20 char *makestring(); /* malloc etc */
21
22 numargs = 0;
23 while ( numargs < MAXARGS )
24 {
25 printf("Arg[%d]? ", numargs);
26 if ( fgets(argbuf, ARGLEN, stdin) && *argbuf != ‘\n‘ )
27 arglist[numargs++] = makestring(argbuf);
28 else
29 {
30 if ( numargs > 0 ){ /* any args? */
31 arglist[numargs]=NULL; /* close list */
32 execute( arglist ); /* do it */
33 numargs = 0; /* and reset */
34 }
35 }
36 }
37 return 0;
38 }
39
40 execute( char *arglist[] )
41 /*
42 * use fork and execvp and wait to do it
43 */
44 {
45 int pid,exitstatus; /* of child */
46
47 pid = fork(); /* make new process */
48 switch( pid ){
49 case -1:
50 perror("fork failed");
51 exit(1);
52 case 0:
53 execvp(arglist[0], arglist); /* do it */
54 perror("execvp failed");
55 exit(1);
56 default:
57 while( wait(&exitstatus) != pid )
58 ;
59 printf("child exited with status %d,%d\n",
60 exitstatus>>8, exitstatus&0377);
61 }
62 }
63 char *makestring( char *buf )
64 /*
65 * trim off newline and create storage for the string
66 */
67 {
68 char *cp, *malloc();
69
70 buf[strlen(buf)-1] = ‘\0‘; /* trim newline */
71 cp = malloc( strlen(buf)+1 ); /* get memory */
72 if ( cp == NULL ){ /* or die */
73 fprintf(stderr,"no memory\n");
74 exit(1);
75 }
76 strcpy(cp, buf); /* copy chars */
77 return cp; /* return ptr */
78 }
一般的 shell框架为:
1 while(TRUE){ //TRUE为1,无限循环
2 read_command(command, parameters); //从终端读取命令
3 if(fork()!=0){ //创建子进程
4 /* Parent code */
5 wait(NULL); //等待子进程结束
6 }esle{
7 /* Child code */
8 exec(command, parameters, 0); //执行命令
9 }
10 }
附注:system函数
函数原型:int system(const char * string);
头文件:#include <stdlib.h>
函数说明:system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命令执行完后随即返回原调用的进程。在调用system()期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。
返回值:如果fork()失败 返回-1:出现错误;如果exec()失败,表示不能执行Shell,返回值相当于Shell执行了exit(127);如果执行成功则返回子Shell的终止状态;如果system()在调用/bin/sh时失败则返回127,其他失败原因返回-1。若参数string为空指针(NULL),则返回非零值>;。如果system()调用成功则最后会返回执行shell命令后的返回值,但是此返回值也有可能为 system()调用/bin/sh失败所返回的127,因此最好能再检查errno 来确认执行成功。
附加说明:在编写具有SUID/SGID权限的程序时请勿使用system(),system()会继承环境变量,通过环境变量可能会造成系统安全的问题。
与exec的区别:
1 #i nclude<stdlib.h>
2 main()
3 {
4 system(“ls -al /etc/passwd /etc/shadow”);
5 }
6 执行结果:
7 -rw-r--r-- 1 root root 705 Sep 3 13 :52 /etc/passwd
8 -r--------- 1 root root 572 Sep 2 15 :34 /etc/shado
9 例2:
10 char tmp[];
11 sprintf(tmp,"/bin/mount -t vfat %s /mnt/usb",dev);
12 system(tmp);
13 其中dev是/dev/sda1.
问题2:如何建立新的进程?
答案2:fork系统调用——复制自己创建一个新进程
一、进程调用fork后,就陷入内核,即执行内核中的do_fork()函数,内核做如下操作:
(1)分配新的内存块和内核数据结构
(2)复制原来的进程到新的进程
(3)向运行进程集添加新的进程
(4)将控制返回给两个进程
当一个进程调用 fork之后,就有两个二进制代码相同的进程,而且它们都运行到相同的地方,但每个进程都将可以开始它们自己的旅程。
二、返回值:根据fork的返回值分辨父子进程,>0即为父进程,=0即为子进程。
(1)父进程中,fork返回新创建子进程的进程ID;
(2)父进程中,fork返回0;
(3)如果出现错误,fork返回一个负值。——fork出错可能有两种原因:一个是当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN;二是系统内存不足,这时errno的值被设置为ENOMEN。
三、父、子进程的区别:
fork的返回值;进程ID;不同的父进程ID;子进程的tms_utime,tms_stime,tms_cutime和tms_ustime设置为0;
父进程设置的锁,子进程不能继承;子进程的未决告警被清楚;子进程的未決信号集被设置为空集。
四、用法:
(1)一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常用的——父进程等待委托者的服务请求,当 这种请求到达时父进程调用fork,使子进程处理此请求,父进程则继续等待下一个服务请求。
(2)一个进程要执行一个不同的程序,这对shell是常见的情况。在这种情况下,子进程在从 fork返回后立即调用exec。
五、fork和vfork:
vfork函数的调用序列和返回值与fork相同,但两者的语义不同。
vfoek与fork一样都创建一个子进程,但vfork并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用 exec(或exit),于是也就不会存访该地址空间。不过在子进程调用exec或exit之前,它在父进程的空间中运行。这种工作方式在某些UNIX的页式虚存实现中提高了效率。
vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。(如果在调用exec或exit之前,子进程依赖于父进程的进一步动作,则会导致死锁)
六、fork小结:
系统调用fork正是解决shell只能运行一条命令这个问题所需要的。使用fork,不但能够创建新的进程,而且能分辨原来的进程和新创建的进程。新的进程能调用execvp来执行任何用户指明的程序。
七、fork()demo:
1 /* forkdemo1.c
2 * shows how fork creates two processes, distinguishable
3 * by the different return values from fork()
4 */
5
6 #include <stdio.h>
7
8 main()
9 {
10 int ret_from_fork, mypid;
11
12 mypid = getpid(); /* who am i? */
13 printf("Before: my pid is %d\n", mypid); /* tell the world */
14
15 ret_from_fork = fork();
16
17 sleep(1);
18 printf("After: my pid is %d, fork() said %d\n",
19 getpid(), ret_from_fork);
20 }
1 /* forkdemo2.c - shows how child processes pick up at the return
2 * from fork() and can execute any code they like,
3 * even fork(). Predict number of lines of output.
4 */
5
6 main()
7 {
8 printf("my pid is %d\n", getpid() );
9 fork();
10 fork();
11 fork();
12 printf("my pid is %d\n", getpid() );
13 }
1 /* forkdemo3.c - shows how the return value from fork()
2 * allows a process to determine whether
3 * it is a child or process
4 */
5
6 #include <stdio.h>
7
8 main()
9 {
10 int fork_rv;
11
12 printf("Before: my pid is %d\n", getpid());
13
14 fork_rv = fork(); /* create new process */
15
16 if ( fork_rv == -1 ) /* check for error */
17 perror("fork");
18
19 else if ( fork_rv == 0 )
20 printf("I am the child. my pid=%d\n", getpid());
21 else
22 printf("I am the parent. my child is %d\n", fork_rv);
23 }
问题3:父进程如何等待子进程的退出?
答案3:pid = wait(&status);
一、系统调用wait做两件事情,首先,wait暂停(阻塞)使用它的进程直到子进程结束;然后,wait通过status取得子进程结束时传给exit的值。
二、返回值:wait返回结束进程的PID。如果调用的进程没有子进程也没有得到终止状态值,则返回-1,同时errno被置为ECHILD。
三、通信:
wait的目的之一是通知父进程子进程结束运行了。另一个目的是告诉父进程子进程是如何结束的。
一个进程以三种方式结束(成功、失败或死亡):
(1)成功:一个进程顺利完成它的任务。按照UNIX惯例,成功的程序调用exit(0)或者从mian函数中return 0。
(2)失败:比如进程可能由于内存耗尽而提前退出程序。按照UNIX惯例,程序遇到问题而退出调用exit时传给它一个非零的值。
(3)死亡:程序可能被一个信号杀死。信号可能来自键盘、间隔计时器、内核或者其它进程。
父进程调用wait时传一个整形变量地址(&status)给函数,内核将子进程的退出状态保存在这个变量中。如果子进程调用exit退出,那么内核把exit的返 回值存到这个整形变量中;如果进程是被杀死的,那么内核将信号序号存放在这个整形变量中。这个整形变量由3个部分组成——8个bit是记录退出 值,7个bit是记录信号序列,另一个bit用来知名发生错误并产生了内核映像(core dump)。
四、wait小结:
五、wait和waitpid
waitpid的提出:如果一个进程有几个子进程,那么只要有一个子进程终止,wait就返回,如果要等待一个指定的进程(知其ID)终止,那该如何做呢?
早期UNIX中,必须调用wait,然后将其返回的ID与所期望的ID作比较。如果终止进程不是所期望的,则将该进程ID和终止状态保存起来,然后再次调用wait。反复这么做直到所期望的进程终止。下一次又想等待一个特定进程时,先查看已终止的进程表,若其中已有要等待的进程,则取其相关信息,否则调用wait。其实我们要等待的是一个特定进程的函数。
函数原型:
pid_t result = wait(int *statusptr)
pid_t waitpid(pid_t pid,int * status,int options);
两者的区别:(1)在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选择项,可使调用者不阻塞。
(2)waitpid并不等待第一个终止的子进程——它有若干个选择项,可以控制它所等待的进程。
参数分析:
pid :欲等待的子进程识别码,其数值意义如下:
pid<-1 等待进程组识别码为 pid 绝对值的任何子进程。
pid=-1 等待任何子进程,相当于 wait()。
pid=0 等待进程组识别码与目前进程相同的任何子进程。
pid>0 等待任何子进程识别码为 pid 的子进程。
status:子进程的结束状态值,可以设置为NULL。
options:参数options提供了一些额外的选项来控制waitpid,参数 option 可以为 0 或可以用"|"运算符把它们连接起来使用。
waitpid额外功能:
(1)waitpid等待一个特定的进程(而wait则返回任一终止子进程的状态)。
(2)waitpid提供了一个wait的非阻塞版本。
(3)waitpid支持作业控制(以WUNTRACED)。
六、wait()demo:
1 /* waitdemo1.c - shows how parent pauses until child finishes
2 */
3
4 #include <stdio.h>
5
6 #define DELAY 2
7
8 main()
9 {
10 int newpid;
11 void child_code(), parent_code();
12
13 printf("before: mypid is %d\n", getpid());
14
15 if ( (newpid = fork()) == -1 )
16 perror("fork");
17 else if ( newpid == 0 )
18 child_code(DELAY);
19 else
20 parent_code(newpid);
21 }
22 /*
23 * new process takes a nap and then exits
24 */
25 void child_code(int delay)
26 {
27 printf("child %d here. will sleep for %d seconds\n", getpid(), delay);
28 sleep(delay);
29 printf("child done. about to exit\n");
30 exit(17);
31 }
32 /*
33 * parent waits for child then prints a message
34 */
35 void parent_code(int childpid)
36 {
37 int wait_rv; /* return value from wait() */
38 wait_rv = wait(NULL);
39 printf("done waiting for %d. Wait returned: %d\n", childpid, wait_rv);
40 }
1 /* waitdemo2.c - shows how parent gets child status
2 */
3
4 #include <stdio.h>
5
6 #define DELAY 5
7
8 main()
9 {
10 int newpid;
11 void child_code(), parent_code();
12
13 printf("before: mypid is %d\n", getpid());
14
15 if ( (newpid = fork()) == -1 )
16 perror("fork");
17 else if ( newpid == 0 )
18 child_code(DELAY);
19 else
20 parent_code(newpid);
21 }
22 /*
23 * new process takes a nap and then exits
24 */
25 void child_code(int delay)
26 {
27 printf("child %d here. will sleep for %d seconds\n", getpid(), delay);
28 sleep(delay);
29 printf("child done. about to exit\n");
30 exit(17);
31 }
32 /*
33 * parent waits for child then prints a message
34 */
35 void parent_code(int childpid)
36 {
37 int wait_rv; /* return value from wait() */
38 int child_status;
39 int high_8, low_7, bit_7;
40
41 wait_rv = wait(&child_status);
42 printf("done waiting for %d. Wait returned: %d\n", childpid, wait_rv);
43
44 high_8 = child_status >> 8; /* 1111 1111 0000 0000 */
45 low_7 = child_status & 0x7F; /* 0000 0000 0111 1111 */
46 bit_7 = child_status & 0x80; /* 0000 0000 1000 0000 */
47 printf("status: exit=%d, sig=%d, core=%d\n", high_8, low_7, bit_7);
48 }
问题4:如何终止一个进程
答案4:系统调用exit
exit是fork的逆操作,进程通过调用exit来停止运行。
无论exit在程序中处于什么位置,只要执行到该系统调用就陷入内核,执行该系统调用对应的内核函数do_exit()。该函数回收与进程相关的各种内核数据结构,把进程的状态置为TASK_ZOMBIE,并把其所有的子进程都托付给init进程,最后调用schedule()函数,选择一个新的进程运行。
exit刷新所有的流,调用由atexit和on_exit注册的函数,执行当前系统定义的其他与exit相关的操作。然后调用_exit。系统调用_exit是一个内核操作,这个操作处理所有分配给这个进程的内存,关闭所有这个进程打开的文件,释放所有内核用来管理和维护这个进程的数据结构。
exit的函数原型:void exit(int status)
exit系统调用带有一个整数类型的参数status,可以利用这个参数传递进程结束时的状态。在实际编程中可以用wait系统调用接受子进程的返回值,从而针对不同的情况进行不同的处理。
注:在一个进程调用了exit之后,该进程并非马上就消失,而是仅仅变为僵尸状态。僵尸状态的进程(僵尸进程)是非常特殊的,虽然它已经放弃了几乎所有的内存空间,没有任何可执行代码,也不能被调度,但它的PCB还没有被释放。僵尸进程的PCB中保存着诸如进程死亡原因、占用的总系统CPU时间、占用的总用户CPU时间、发生缺页中断的次数和收到信号的数目等。我们可以通过wait系统调用收集僵尸进程留下的信息,同时使这个进程彻底的消失。
进程终止的五种方式小结:
(1)正常终止
(a)从main返回。从main函数中执行return语句,等效于调用exit。
(b)调用exit。此函数由ANSI C定义,其操作包括调用各终止处理程序(终止处理程序在调用atexit函数时登录) 然后关闭所有标准I/O流等,因为ANSI C并不处理文件描述符、多进程(父、子进程)以及作业控制,所以这一定义对 NIX系统而言是不完整的。
(c)调用_exit。此函数由 e x i t调用,它处理 U N I X特定的细节。 _exit是由POSIX.1说明的。
(2)异常终止
(a)调用abort。它产生SIGABRT信号,所以是下一种异常终止的一种特例。
(b)由一个信号终止。进程本身(例如调用abort函数)、其他进程和内核都能产生传送到某一进程的信号。例如,进程越出其地址空间访问存储单元,或者除以 0,内核就会为该进程产生相应的信号。
_exit()小结:
系统调用_exit()终止当前进程并执行所有必须的清理工作,其操作包括:
(1)关闭所有文件描述符和目录描述符;
(2)将该进程的PID置为init进程的PID;
(3)如果父进程调用wait或waitpid来等待子进程结束,则通知父进程;
(4)向父进程发送SIGCHLD。
exit和_exit:
exit和_exit函数用于正常终止一个程序: _exit立即进入内核,,exit则先执行一些清除处理(包括调用执行各终止处理程序,关闭所有标准 I/O流等),然后进入内核。#include <stdlib.h> void exit (int status)以及#include <unistd.h> void _exit(int status);使用不同的头文件,exit是由ANSI C 说明的,而_exit则是由POSIX.1说明的。exit和_exit都带一个整型参数,称之为终止状态( exit status)。
exit()demo:
1 #include <sys/types.h>
2 #include <unistd.h>
3
4 main()
5 {
6 pit_t pid;
7 pid = fork();
8 if(pid < 0)
9 printf("error occurred!\n");
10 else if(pid == 0)
11 exit(0);
12 else
13 {
14 sleep(60); //睡眠60秒,这段时间内,父进程什么也干不了
15 wait(NULL); //收集僵尸进程的信息
16 }
17 }
小结:shell是如何运行一个程序的?
shell用fork建立新进程,用exec在新进程中运行用户指定的程序,最后shell用wait等待新进程结束。wait系统调用同时从内核取得退出状态或者信号序列以告知子进程是如何结束的。
时间轴
1.打印提示符 2.取得命令 3.建立新进程 4.等待子进程 5.得到子进程状态
4.运行新进程 5.新进程在运行 6.新进程结束
进程的一生
随着一句fork,一个新进程呱呱落地,但这时它只是老进程的一个克隆。然后随着exec,新进程脱胎换骨,离家出走,开始了独立工作的职业生涯。
人有生老病死,进程也一样,它可以是自然死亡,即运行到main函数的最后一个"}",从容的离我们而去;也可以是中途退场,退场有两种方式,一种是调用exit函数,一种是在main函数内使用return,无论哪一种,它都可以留下遗言,放在返回值里面保留下来;甚至它还可能被谋杀,被其它进程通过另外一些方式结束它的生命。
进程死掉以后,后留下一个空壳,wait站好最后一班岗,打扫战场,使其最终归于无形。
这就是进程的一生。
原文:http://www.cnblogs.com/xymqx/p/3710307.html