8.1 引言
包括创建新进程,执行程序和终止。还将介绍进程属性中的各种ID——实际、有效和保存的用户ID和组ID.
8.2 进程标识/进程ID
#include <unistd.h>
pid_t getpid(void);//获取进程ID
pid_t getppid(void);//获取父进程ID
pid_t getuid(void);//获取进程的实际用户ID
pid_t geteuid(void);//获取进程的有效用户ID
pid_t getgid(void);//获取进程的组ID
pid_t getegid(void);//调用进程的有效组ID
ID为0的进程是调度进程,是系统进程
,不执行任何磁盘上的程序.
ID为1的进程是init
进程,该进程不会终止,是一个普通的用户进程,但是以超级用户特权进行,是所有孤儿进程
的父进程.
8.3 fork创建新进程
#include <unistd.h>
pid_t fork(void);
//父进程返回子进程ID,子进程返回0。
//若失败返回 -1
子进程和父进程继续执行fork之后的指令,子进程是父进程的副本,得到了父进程的数据空间、堆栈空间的副本,并不共享这部分空间.
事实上,创建子进程后并不是马上创建一个完全副本,内核将这些区域权限改为只读,两者共享内存,如果父进程或者子进程试图修改某一块内存,内核才会只为修改的那一块制作一个副本,这被称为写时复制
。
实例:
#include <unistd.h>
#include <stdio.h>
int gvar = 6;
char buf[] = "a write to stdout!\n";
int main()
{
int var = 88;
pid_t pid;
if(write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1 )
printf("before fork.\n");
if((pid = fork()) < 0)
{
printf("fork failed!\n");
}
else if(pid == 0)//子进程
{
gvar++;
var++;
}
else//父进程
{
sleep(2);
}
printf("pid = %ld, gvar = %d, var = %d\n", getpid(), gvar, var);
return 0;
}
write是不带缓冲的,所以fork之前调用数据写道标准输出一次。
但当标准IO形式带缓冲,如果在fork时,该行数据还在缓冲区中,会被复制到子进程,父子进程各自有带该行内容的缓冲区.
文件共享
fork
函数的用法:
- 父进程希望复制自己,使父子进程执行不同的代码。服务器在请求到达时调用fork使子进程处理此请求,父进程继续等待下一个请求。
- 一个进程要执行一个不同的程序。对shell来说,子进程从fork返回后立即调用exec.补充exec
8.4 函数vfork
vfork也是创建一个新进程,但和fork的区别是:
- 不将父进程的地址空间完全复制到子进程。在子进程调用exec或exit之前,子进程在父进程地址空间运行。
- vfork保证子进程先运行,在它调用exec或者exit后父进程才会运行.
#include <unistd.h>
#include <stdio.h>
int gvar = 6;
int main()
{
int var = 88;
pid_t pid;
printf("before fork.\n");
if((pid = vfork()) < 0)
{
printf("vfork failed!\n");
}
else if(pid == 0)//子进程
{
gvar++;
var++;
_exit(0);
}
//这时父进程不需要sleep,因为子进程保证先运行.
printf("pid = %ld, gvar = %d, var = %d\n", getpid(), gvar, var);
return 0;
}
得到:
before fork
pid = 29039, gvar =7, var = 89
调用_exit,而不是exit(),是因为_exit不会执行标准IO缓冲区的冲洗操作,执行exit的结果则是不确定的.
如果exit实现的是关闭IO流,那么标准输出FILE对象的相关存储区将被清0,但是当exit实现的是冲洗IO流的话,则输出应该就是等同于上面。
8.5 exit
exit
是函数正常终止的方法,事实上,exit
会调用_exit
函数。进程正常终止时会将其退出状态
传递给函数,异常终止时,内核产生终止状态
。
而且执行_exit后,内核将退出状态
转换为终止状态
。
无论哪种终止,该终止进程的父进程都可以使用wait
或者waitpid
获取到其终止状态。
孤儿进程:
如果父进程在子进程之前终止,则其所有子进程的父进程都会改变为init进程
。机制是:如果一个进程终止,则内核检查所有进程是否是该进程的子进程,若是则该进程的父进程ID更改为1(init
进程的ID为1).
僵尸进程:
内核为每个终止子进程保存了一些信息,至少包括进程ID和终止状态等。父进程可以调用wait
或者waitpid
得到终止子进程,然后释放资源。
但是当子进程已经终止,但父进程没有获取得到其终止信息并释放资源,则该终止子进程将成为僵尸进程。
8.6 wait和waitpid
当一个进程终止时,内核会向父进程发送SIGCHILD异步信号,父进程可以选择忽略或者提供一个该信号发生时即被调用执行的函数.
调用wait或者waitpid可能发生:
- 所有子进程还在运行,则阻塞
- 一个子进程终止,正等待父进程获取其状态,则取得该子进程的终止状态后返回进程ID
- 如果没有子进程则出错返回
参数:
返回值:
子进程ID
入参:
pid: waitpid函数可以指定等待一个进程终止
stat: 存放进程的终止状态,可传空指针
options: 可使调用者不阻塞
#include <sys/wait.h>
pid_t wait(int *stat);
pid_t waitpid(pid_t pid, int *stat, int options);
返回的stat由系统实现的宏负责检测:
8.6.1 实例1 wait
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
int main(void){
pid_t pid;
int status;
if((pid == fork()) < 0)
printf("fork error\n");
else if(pid == 0)
exit(7);//子进程终止,则子进程不会运行下面的代码
if(wait(&status) != pid)
printf("wait error\n");
/*使用宏检查终止状态代码省略*/
return 0;
}
8.6.2 实例僵尸进程
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid;
if((pid = fork()) < 0)
printf("fork error\n");
else if(pid == 0)//子进程里面的操作
{
//在child里创建child
if((pid = fork()) < 0)
printf("fork error\n");
else if(pid > 0)//父进程退出
exit(0);
sleep(2);
printf("second child, parent pid = %ld.\n", getppid());
exit(0);//子子进程也退出
}
if(waitpid(pid, NULL, 0) != pid)//父进程等待第一个子进程
printf("wait error\n");
return 0;
}
输出结果
second child, parent pid = .
可以得到当子进程终止时,子子进程的父进程变为了init
进程(pid = 1)
使用sleep的目的是为了防止第一个子进程没有完全终止,使得子子进程打印父进程ID得到的就不是init了,当然这种sleep的保证也不是完全可以保证的.
8.9 waitid
作用:取得进程终止状态, 比waitpid还要更灵活
#include <sys/wait.h>
int waitid(idtype type, id_t id, siginfo_t *info, int options);
waitid允许等待一个特定进程,但参数与waitpid稍有不同
type参数
options参数
WCONTINUED、WEXITED或WSTOPPED这三个常量之一必须在options里指定.
8.8 wait3 和wait4
和wait\waitpid\waitid
提供的功能类似,但是该函数参数支持返回其终止进程及其所有子进程的使用资源情况.
资源使用情况包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等。
8.10 exec
fork
创建子进程后,需要使用exec
函数以执行另一程序,否则子进程则会和父进程拥有相同的内容,只是使用if else
区分.
调用exec函数后,该进程执行的程序完全替换为新进程,替换了当前进程的代码段、数据段和堆栈.
#include <unistd.h>
int execl(char *pathname, char *arg0, ..., (char*)0);
int execv(char *pathname, char *argv[]);
int execle(char *pathname, char *arg0, ...);
int execve(char *pathname, char *argv[], char *envp[]);
int execlp(char *filenme, char *arg0, ..., (char*)0);
int execvp(char *fiename, char *argv[]);
int fexecve(int fd, char *argv[], char *envp[]);
//若出错返回-1, 成功不返回
使用filename
时,默认在PATH
环境变量下搜寻可执行文件,若filename
中包含了/
,则视为路径名.
exec函数实例
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
const *env_init[] = {"USER=unknown", "PATH=/tmp", NULL};
int main()
{
pid_t pid;
if((pid = fork()) < 0)
printf("fork error.\n");
else if(pid == 0)
{
if(execle("/home/sar/bin/echoall", "arg1", "arg1", "arg3", (char*)0, env_init) < 0);
printf("exce error.\n");
}
if(waitpid(pid, NULL, 0) < 0)
printf("wait error.\n");
exit(0);
}
8.16 进程调度
UNIX历史上只是进行基于优先级的进程调度,进程可以通过调整友好值选择以更低的优先级运行,调整友好值以降低对CPU的占用,所以是有好的。友好值越高,进程优先级越低。
进程通过nice函数获取或更改友好值:
nice
参数inch是以加的形式改变进程优先级,可正可负
#include <unistd.h>
int nice(int inch);
//成功返回新的友好值,失败返回-1.
由于-1
也是合法的返回值,所以调用nice
前先清楚除erno
(errno = 0
),函数返回-1后检查errno
,若不为0则nice
调用失败。
====================================================
getpriority
getpriority还可以获取一组相关进程的友好值.
#include <sys/resource.h>
int getpriority(int which, id_t who);
//成功返回友好值,失败返回-1
which参数说明是哪种方式:
- PRIO_PROCESS表示jincheng
- PRIO_PGRP表示进程组
- PRIO_USER表示用户ID
who参数取决于which参数的取值,说明取值:
如果which参数作用于多个进程,则返回所有进程中优先级最高的(最小的友好值).
setpriority
为进程、进程组和属于特定用户ID的所有进程设置优先级,他的参数与getpriority
函数同一逻辑.
#include <resource.h>
int setprority(int which, int which);