5穿插章节:进程API
前言:该穿插章节将更多的介绍有关系统的实际运用,特别针对于操作系统API,以及如何运用它们。如果你不喜欢实践的东西,你可以跳过这个章节。但是,你最好应该关注一下实践,因为在实际生活中经常用到它们;毕竟,如果你没有实践技能,用人公司一般是不会录用你的。
在这个章,我们讨论Unix系统中的进程创建。Unix有种十分有趣的新建进程的方法,系统调用fork()和exec()。还有wait(),用于进程等待另一个进程的结束。现在,我们就用一些例子来讲解关于这些函数的更多细节。至此,我们提出问题:
/*******
Crux:怎样创建和控制进程
操作系统应该提供什么接口用于进程创建和控制?
这些接口应该如何设计才能简单易用?
********/
5.1系统调用:fork()
fork()函数通常用于创建一个新的进程。不过要注意的是,这可能是你调用的最奇怪的方法。更确切点,你有一个如下所示的程序;看一下这段代码,或者最好自己敲一下并运行。
1#include<stdio.h>
2#include<stdlib.h>
3#include<unistd.h>
4
5int
6main(int argc, char*argv[])
7{
8printf("helloworld (pid:%d)\n", (int) getpid());
9int rc = fork();
10if (rc < 0) { //fork failed; exit
11fprintf(stderr,"fork failed\n");
12exit(1);
13} else if (rc == 0){ // child (new process)
14printf("hello,I am child (pid:%d)\n", (int) getpid());
15} else { // parentgoes down this path (main)
16printf("hello,I am parent of %d (pid:%d)\n",
17rc, (int) getpid());
18}
19return 0;
20 }
当你运行这个程序(程序名:p1.c)时,你会看到如下输出:
prompt>./p1
helloworld (pid:29146)
hello,I am parent of 29147 (pid:29146)
hello,I am child (pid:29147)
prompt>
/**********************************/
让我们来理解一下程序P1.c的内容。当它首次运行时,进程打印了一条helloworld信息;在这条信息后面的是进程号,即所谓的PID。该进程的进程号是29146,在Unix系统中,如果一个人想要对这个进程做什么的话,就可以根据进程号来进行操作,比如终止这个进程。迄今,这个法子都很好用。
至此,最好玩的部分要开始了。在这个进程中,使用了一个系统调用fork()来创建一个新进程。奇怪的是,新创建的进程几乎就和原进程一样。这就意味着,对操作系统来说,此刻有两个同样的p1程序再运行,他们都是返回fork()函数的调用。如你所愿,新创建的进程(一般称为子进程,相对应的程创建该子进程的那个进程为父进程)并不是在main()函数处就开始运行的(注意,helloworld信息只打印了一次);子进程在fork()进程调用以后才产生的。
也许你已经注意到了,子进程并不是完全复制的父进程。尤其是,尽管它复制了地址空间(它私有的内存),私有的寄存器,计算器等等,但是通过fork()函数的调用,它返回的进程号却是不同的。尤其是,当父进程接收到子进程的PID以后,子进程就返回了0而已。这个不同点是很有用的,这样就方便在两个进程里面写不同的代码了。
1#include<stdio.h>
2#include<stdlib.h>
3#include<unistd.h>
4#include<sys/wait.h>
5
6int
7main(int argc, char*argv[])
8{
9printf("helloworld (pid:%d)\n", (int) getpid());
10int rc = fork();
11if (rc < 0) { //fork failed; exit
12fprintf(stderr,"fork failed\n");
13exit(1);
14} else if (rc == 0){ // child (new process)
15printf("hello,I am child (pid:%d)\n", (int) getpid());
16} else { // parentgoes down this path (main)
17int wc = wait(NULL);
18printf("hello,I am parent of %d (wc:%d) (pid:%d)\n",
19rc, wc, (int)getpid());
20}
21return 0;
22 }
程序P2.c调用fork()函数和wait()函数
这里你应该也注意到了,程序(p1.c)的输出并不是确定的。当子进程被创建以后,在系统中我们就有两个运行态进程了:父进程和子进程。假设我们在单CPU系统上运行这个程序,父子进程只能在同一时刻运行其中一个。在我们的例子中(如上),父进程首先开始运行并打印它的输出信息。在其他例子中,也可能有相反的情况发生,如我们所示的输出跟踪:
prompt>./p1
helloworld (pid:29146)
hello,I am child (pid:29147)
hello,I am parent of 29147 (pid:29146)
prompt>
很快我们就会在后面对CPU的调度进行详细的讲解,这里CPU调度决定了在确定时间运行哪个进程。由于调度十分复杂,因此我们不能很有把握的预测出CPU到底会先运行哪个进程。这种不确定性,产生了很多好玩的问题,尤其是在多线程程序中;因此当我们本书的第二部分——并发的章节中,我们会对这种不确定性有更进一步的了解。