进程控制
进程控制介绍
进程控制中涉及到进程创建、睡眠、退出等,在Linux中提供fork、clone的进程创建方法,sleep的进程睡眠,exit的进程终止调用。
主要的系统调用
下面将具体介绍重要的系统调用的代码实现。
fork创建进程
我们可输入man 2 fork查看该函数的声明
由图可知函数声明在头文件<unistd.h>中,且fork的类型为pid_t,这个类型与进程标识符PID的类型是一样的,所以我们这样写代码也是OK的:
pid_t pid;
pid = fork();
fork的中文名为分叉,十分形象。在每次执行fork后,就会产生一个新的进程,这样就分叉了,当前进程称为子进程,原来的进程称为父进程。
fork的特点在于一次性返回两个值。先来分析下面的代码:
int main() { int i; if (fork() == 0) { for (i = 1; i < 3; i++) printf("This is child process\n"); } else { for (i = 1; i < 3; i++) printf("This is parent process\n"); } }
执行结果如下:
This is child process This is child process This is parent process This is parent process
if和else里头的代码都执行了,说明fork()的返回值必然一个为0另一个为非0。
通过查阅资料知道,在执行fork()时,由于fork创建了一个子进程,所以fork()会将子进程的PID(进程标识符为一个正值)返回给父进程,而将0返回给刚创建的子进程。如果想得到父进程的PID,用函数getppid(),如果想得到子进程的PID,则用函数getpid(),这两个函数的返回值类型都是int。
额外补充一点,fork()创建失败的话,返回值为-1,失败的具体情况有下两点:
- 父进程拥有的子进程个数超过了限制。
- 提供使用的内存不足
另外我们把for循环的次数放大,比如每个for循环执行5000次,会发现一个有趣的现象,即开始交错打印:
则明显地看到父进程与子进程的并发执行。一般来说是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。但是由于操作系统一般让所有进程都享有相同的执行权,除非另一个优先权更高。所以这里的父进程与子进程的执行会交替进行。
如何避免父进程与子进程的并发执行呢?可以使用vfork()函数,它的语法与fork()一致。vfork的特性之一是保证子进程先执行,等到它调用exit();或者exec的时候,父进程才可以调度执行。
vfork的另一个特性是用其创建的子进程共享父进程的地址空间,也就是说子进程完全运行在父进程的地址空间之上。若子进程与父进程有相同的局部变量、全局变量,那么子进程修改任一变量的值,父进程的对应变量的值也被修改。而fork则不同,子进程与父进程是独立的,这就是说子进程里修改任一变量的值,父进程是不受影响的。而且正因为fork创建的子进程的地址空间的与父进程的地址空间是彼此独立,所以父进程才要将几乎是一切的资源都拷入子进程中,这是非常浪费的行为。
再说一个fork的特性,就是子进程会继承父进程的数据段、堆栈段,还有缓冲区。这意味着如果你不刷新缓冲区,之前在父进程缓冲区中的数据还会被继承到子进程中。刷新缓冲区的办法有:1、printf中的\n 2.fflush(stdout); 。
举例:
//例一 printf("hello "); fork(); printf("world "); //最后输出hello world hello world //例二 printf("hello "); fflush(stdout); fork(); printf("world "); //最后输出hello world world //例三 printf("hello\n"); fork(); printf("world "); //最后输出hello world world
exit进程退出
Linux系统中进程退出的方式分为正常退出和异常退出两种,其中正常退出的方法有三种,异常退出的方法有两种。
正常退出: 1.main函数中的return 2.调用exit 3.调用_exit
异常退出:1.调用about 2.进程接收到某个信号而终止
return与exit的区别是:exit是有参数,return是执行完函数后的返回;exit结束后将控制权交给系统,return结束后将控制权交给调用函数。
exit与_exit的区别是:exit在头文件stdlib.h声明,_exit在头文件unistd.h声明。结束进程后,_exit会立即返回给内核,而exit要清除一段指令后才返回给内核。
exec进程执行新程序
一般在子进程中会使用exec函数来执行另一个程序。系统调用exec用于执行一个可执行程序以代替当前的程序,也就是说某进程一旦调用了 exec 类函数,正在执行的程序就被干掉了,系统把代码段替换成新的程序(由 exec 类函数执行)的代码,并且原有的数据段和堆栈段也被废弃,新的数据段与堆栈段被分配,但是进程号却被保留。
所以exec的执行结果为:系统认为该进程还是原来的进程,但是进程里面的程序被替换了。
下面是通过man 3 exec得到的exec函数族的相关信息
将上面的库函数声明归纳后,如下图
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1,所以exec
函数只有出错的返回值而没有成功的返回值。
这儿出现了一个陌生的外部全局变量 char **environ,这是什么呢? 它是环境变量。
所谓环境变量,就是为了便于用户灵活地使用shell,Linux引入了环境变量的概念,包括了用户的主目录、当前目录、终端类型等,它们定义了用户的工作环境,所以称为环境变量。通过命令env可查看这些环境变量值,或者通过以下方式也能实现查看环境变量值。
extern char **environ; for(i=0; environ[i] != NULL; i++) printf("%s",environ[i]);
另外,事实上main函数的完整形式是:int main(int argc, char *argv[], char **envp)。
关于exec函数族的调用举例:
事实上,只有execve
是真正的系统调用,其它五个函数最终都调用execve
,这些函数之间的关系如下图所示:
实例:(实现IO重定向)把标准输入转成大写然后打印到标准输出
分析:调用exec后,原来打开的文件描述符仍然是打开的。利用这一点可以实现I/O重定向。
/* upper.c */ #include <stdio.h> int main(void) { int ch; while((ch = getchar()) != EOF) { putchar(toupper(ch)); } return 0; }
/* wrapper.c */ #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <fcntl.h> int main(int argc, char *argv[]) { int fd; if (argc != 2) { fputs("usage: wrapper file\n", stderr); exit(1); } fd = open(argv[1], O_RDONLY); if(fd<0) { perror("open"); exit(1); } dup2(fd, STDIN_FILENO); close(fd); execl("./upper", "upper", NULL); perror("exec ./upper"); exit(1); }
wrapper
程序将命令行参数当作文件名打开,将标准输入重定向到这个文件,然后调用exec
执行upper
程序,这时原来打开的文件描述符仍然是打开的,upper
程序只负责从标准输入读入字符转成大写,并不关心标准输入对应的是文件还是终端。
wait等待进程结束
当子进程先于父进程退出时,若无调用wait()或waitpid(),子进程就会进入僵尸进程。若调用,则不会变为僵尸进程。
系统调用声明如下:
wait():系统让父进程暂停执行直到它的一个子进程停止。返回值是该子进程的PID。
waitpid():与wait同理,不过它是让特定的的子进程停止且需要提供PID。参数pid指明了要等待的子进程的PID。pid值的意义见下表。options 参数允许用户可改变waitpid()的行为,若将该参数赋值为WNOHANG,则父进程不被挂起而立即返回并执行其后的代码。
它们都有int *status这个参数,这个参数指向的变量存放了子进程的(状态信息),比如子进程main()返回的值或者子进程中exit()的参数。
让父进程周期性检查特定的子进程是否已经退出:
waitpid(child_pid, (int*)0, WNOHANG );
如果子进程未退出,则返回0,若子进程已退出,则返回child_pid。调用失败则返回-1。失败的原因可能是子进程不存在、参数不合法等。
注意:waitpid是wait的非阻塞版本,如果希望父进程在查看子进程状态的同时不被阻塞,可使用waitpid与WNOHANG选项。
未完待续