进程控制主要包括创建新进程,执行程序和进程的终止等一些控制函数。
一、获取进程ID
每个进程都有一个非负整数型表示唯一的进程ID。
#include <sys/types.h>
#include<unistd.h>
pid_t getpid(void); //返回调用进程的进程ID
pid_t getppid(void);//返回它的父进程的进程ID
二、创建和终止进程
进程有三种状态:
运行:要么在CPU上运行,要么等待被执行且最终被内核调度
停止:进程的执行被挂起,且不会被调度
终止:进程永远停止。有三个原因
收到一个信号,该信号的默认行为是终止进程
从主程序返回
调用exit函数
#include <stdlib.h>
void exit(int status); //status是退出状态码
父进程通过fork函数来创建一个子进程
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
新创建的子进程得到与父进程虚拟地址空间相同(但是独立的一份副本),包括代码和数据段,堆,共享库和用户栈。还获得父进程所有打开文件描述符的副本,这样子进程可以读写父进程中任何打开的文件。父进程和子进程最大的不同是他们有不同的PID。
Fork函数调用一次,却返回两次,在父进程中返回子进程的PID, 在子进程中,返回0。因为子进程的PID总是非0的,这样通过返回值就可以区分是在父进程中运行还是在子进程中运行。
1 #include "apue.h"
2
3 int glob = 6;//全局变量
4 char buf[] = "a write to stdout\n";
5 int main(void)
6 {
7 int var;
8 pid_t pid;
9 var = 88;//栈上自动变量
10 if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
11 err_sys("write error");
12 printf("before fork\n");
13
14 if((pid = fork()) < 0)
15 {
16 err_sys("fork error");
17 }
18 else if(pid == 0)
19 {
20 glob++;
21 var++;
22 }
23 else
24 {
25 sleep(2);
26 }
27 printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);
28 exit(0);
29 }
将程序用标准输出来输出,可以看到,子进程对glob, var的改变在父进程中没有体现:
将结果重定向到fork文件中,可以看到“before fork”出现了两次:
这是apue中的一个例子,很经典,可以说明三个问题:
1. 子进程对变量全局变量glob和main函数内部的在栈区存储的自动变量var的改变并不影响父进程中该变量的值。这是因为在父进程fork一个子进程后,子进程会共享代码段,但是会拷贝数据段和用户栈等,而glob位于数据段,var位于用户栈,所以子进程对它们的改变不会影响父进程中的值。
2. 输出到标准输出和打印到文件中的区别(缓冲机制)
第一次标准输出
一开始父进程执行printf(“before fork\n”)打印,所以出现了before fork语句,由于这是标准输出,是行缓冲机制,所以在用\n进行换行时系统就会把缓冲区中的数据冲洗掉,所以子进程继承得到的缓冲区将为空,然后程序执行fork,子进程先执行(父进程sleep了2秒),指向++操作后,利用printf打印操作数的值也就是:
pid = 6856, glob = 7, var = 89
然后子进程执行exit函数退出程序,在退出程序的时候会清理缓冲区,所以会检查子进程中缓冲区的内容,如果存在则写会输出上,但是子进程继承来的缓冲区已经被冲洗掉了,不存在数据,所以exit退出时子进程将不会打印
before fork !
最后父进程执行打印操作数的语句:
pid = 6855, glob = 6, var = 88
第二次将其打印到文件中
由于打印到文件中,文件不会去识别\n就去冲洗掉内存缓冲区 ,,所以在子进程调用exit函数退出程序时,就会把缓冲区中的数据再次打印到文件中,然后退出。然后父进程打印退出!
3. 在重定向父进程的标准输出时,子进程的标准输出也被重定向了,这是因为fork的特性使得父进程所有的打开文件描述符都被复制到了子进程中。
三、回收子进程
当一个进程由于某种原因而终止时,都会执行内核中的一段代码,这段代码为这个进程关闭所有打开的描述符,释放他所使用的存储器。内核还不会立即从系统中把它清除出去,而是保持在一种已终止的状态,直到它被父进程回收。如果它的父进程也终止了,它就成为了孤儿进程,内核会安排init进程作为它的养父。
一个进程可以调用waitpid或者wait函数来等待它的子进程终止或者停止。
#include <sys/types.h>
#inlcude <sys/wait.h>
pid_t waitpid(pid_t pid, int *statusp, int options);
pid_t wait(int *statusp);
返回:若成功,返回子进程的PID,若为WNOHANG, 则为0,错误,为-1
参数说明
1.pid是等待集合的成员 ,若pid>0, 则等待集合就是进程ID等于pid的一个单独的进程。若pid=-1,等待集合就是由父进程的所有子进程组成。
2. options是用来修改默认行为
通常设置常量为WNOHANG, WUNTRACESD, WCONTINUED的各种组合来修改默认行为。
默认情况下(options = 0),waitpid会挂起调用进程的执行,知道它的等待集合中的一个子进程终止。如果它的等待集合中的一个进程在调用时就已经终止了,那么waitpid就会立即返回。这两种情况中,waitpid返回导致waitpid返回已终止子进程的PID。
WNOHANG: 若等待集合中任何子进程都还没有终止,那么就立即返回(返回0)。
WUNTRACED: 挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止。
WCONTINUED: 挂起调用进程的执行,直到等待集合中的一个正在运行的进程终止或者等待集合中一个被停止的进程收到SIGCONT信号重新开始执行。
3. statusp:检查已回收子进程的退出状态
若statusp参数非空,则waitpid会在status中放上导致返回子进程的状态信息,status是statusp指向的值。
wait是waitpid的简单版本,等价于waitpid(-1, &status, 0)
四、加载并运行程序
exec函数族有7个。是在当前进程的上下文中加载并运行一个程序。
这里要注意程序和进程的区别:程序是一堆代码和数据的集合,可以作为目标文件存在磁盘上,或者作为段存在于磁盘中,程序总是运行在某个进程的上下文中。
当进程调用一种exec函数是,该进程执行的程序完全替换为新程序,新程序从main函数开始执行。调用exec并不创建新进程,所以前后的进程ID并没有改变,exec只是用磁盘上的一个新程序替换了当前进程的正文段,数据段,堆段和栈段。
int execl(const char *pathname, const char *arg, ...)
int execv(const char *pathname, char *const argv[])
int execle(const char *pathname, const char *arg, ..., char *const envp[])
int execve(const char *pathname, char *const argv[], char *const envp[])
int execlp(const char *filename, const char *arg, ...)
int execvp(const char *filename, char *const argv[])
int fexecve(int fd, char *const argv[], char *const envp[])
若成功,不返回,出错,返回-1
这四个函数的区别是前四个函数去路径名为参数,后两个函数取文件名为参数,最后一个取文件描述符为参数。
• 含有 l 和 v 的 exec 函数的参数表传递方式是不同的。
• 含有 e 结尾的 exec 函数会传递一个环境变量列表。
• 含有 p 结尾的 exec 函数取的是新程序的文件名作为参数,而其他exec 函数取的是新程序的路径。
当指定文件名(filename)为参数时(也就是execlp和execvp函数):
如果filename中包含/, 则就视其为路径名
如果不包含/, 就在PATH环境变量,在环境变量所指的各目录搜寻可执行文件
PATH变量包含了一张目录表,目录之间用冒号(:)分隔,
PATH = /bin:/usr/bin:/usr/local/bin:.
.表示当前目录
一个加载函数的例子
#include "apue.h"
int
main(int argc, char *argv[])
{
int i;
char **ptr;
extern char **environ;
for (i=0; i < argc; i++)
printf("argv[%d] : %s \n", i, argv[i]);
for (ptr = environ; *ptr != 0; ptr++)
printf("%s\n", *ptr);
exit(0);
}
这个程序很简单,用来回显所有命令行参数及全部环境表。将它生成可执行程序echoall
然后有如下代码加载这个程序echoall
#include "apue.h"
#include <sys/wait.h>
// 环境变量列表是一个指向NULL结尾的指针数组,每一个指针指向一个环境变量字符串,每个串都是形如“name = value”的键值对
char *env_init[] = {"USER = unknown", "PATH = /tmp", NULL};
int main(void)
{
pid_t pid;
if ((pid = fork()) < 0){
err_sys("fork error");
}else if(pid == 0){
// 从路径名加载, echoall_1是argv[0], 代表可执行目标文件的名字
if (execle("/home/friday/apueCode/echoall", "echoall_1", "myarg1", "MY ARG2", (char *)0, env_init) < 0)
err_sys("execle error");
}
if (waitpid (pid, NULL, 0) < 0)
err_sys("wait error");
if ((pid = fork()) < 0){
err_sys("fork error");
}else if (pid == 0){
// 从文件名加载,echoall_2是argv[0], 代表可执行目标文件的名字
if (execlp("echoall", "echoall_2", "only 1 arg", (char *)0) < 0)
err_sys("execlp error");
}
exit(0);
}
第一个函数execle是从路径名加载程序,echoall_1是argv[0], 代表可执行目标文件的名字,”myarg1”, “MY ARG2”是传给这个echoall程序的两个参数,这个execle函数还要接受一个环境变量列表。
第二个函数在环境变量PATH中寻找可执行文件echoall, 初始运行时可能出现“no such file”的问题,这是因为在PATH中没有echoall所在的路径。解决方法是加入临时环境变量。然后就可以运行: