1.进程标识
唯一,且可复用,延迟复用算法
ID为0的是 调度进程(交换进程) 内核中的系统进程
ID为1的是init进程,在自举过程中年由内核调用。此进程负责在自举内核之后启动一个unix系统,通常读取与系统有关的初始化文件(/etc/rc* ),并将系统引导到一个状态(多用户)。init进程绝不会终止,且为用户进程,但是以超级用户特权运行,init是所有孤儿进程的父进程。
2.fork函数
#include <unistd.h>
pid_t fork(void)
由fork创建的新进程被称为子进程(child process)。
调用一次,返回两次。两次返回的区别是:子进程的返回值是0,而父进程的返回值则是新建子进程的进程ID。
一个进程的子进程可以有多个,并且没有一个函数可以获得父进程所有的子进程ID。
为什么子进程的返回值是0?一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得父进程的进程ID。
子进程是父进程的副本。子进程获得父进程的数据空间,堆和栈。但是两者并不共享存储空间,只共享正文段。
#include "apue.h"
int globval =6;
char buf[] = "a write to stdout \n";
int
main(void)
{
int var;
pid_t pid;
var = 88;
if(write(STDOUT_FILENO,buf,sizeof(buf)-1) != sizeof(buf)-1) // sizeof buff 计算的值包括终止null 所以-1 strlen函数不包含终止值null,且sizeof不需要进行函数调用,在编译器就计算出结果了。
err_sys("write error");
printf("before fork\n");
if((pid = fork()) < 0)
{
err_sys("fork error");
}
else if(pid == 0)
{
globval++;
var++;
}
else
{
sleep(2);
}
printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar, var);
exit(0);
}
结果
注意结果
如果标准输出连到终端设备,那么它是行缓冲的,否则是全缓冲的
但是write函数是不带缓冲的。
当以交互方式(在终端运行)运行程序的时候,只得到printf输出的一行,是因为标准输出缓冲由换行符冲洗。但是如果当标准输出重定向到一个文件的时候,却得到printf输出行两次,原因是在fork之前调用了printf一次,当调用fork时,改行仍在缓冲区中,然后将父进程的数据空间复制到子进程中的时候,缓冲区的数据也复制到子进程中了,所以子进程也有了printf的缓存数据。在exit之前的第二个printf将其追加到已有的缓冲区中。
文件共享:
在重定向父进程的标准输出的时候,子进程的标准输出也被重定向。
fork的一个特性就是所有打开文件描述符都会被复制到子进程中
在fork之后处理文件描述符有两种常见情况:
1.父进程等待子进程完成
2.父进程和子进程各自执行不同的程序段,在这种情况下fork之后,父进程和子进程各自关闭他们不需要使用的文件描述符。
3.父进程和子进程的区别
-1.fork的返回值不同
-2.进程id不同
-3.两个进程的父进程id不同:子进程的的父进程ID是创建它的进程的ID,父进程的ID不变
-4.子进程的tms_utime,tms_stime,tms_cutime,tms_ustime的值设置为0
-5.子进程不继承父进程设置的文件锁
-6.子进程未处理闹钟被清除
-7.子进程未处理信号集设置为空集
4.fork失败的原因
1.系统进程太多
2.该用户ID的进程数超过了系统限制。CHILD_MAX规定了每个实际用户ID在任一时刻可拥有的最大进程数。
5.fork的用法
1.父进程希望复制自己,使得父进程和子进程同时执行不同的代码段。
2.一个进程要执行一个不同的程序。shell就是这样的,这种情况下子进程从fork返回后立刻调用exec
7.vfork函数
函数的调用序列和返回值和fork相同。
用于创建一个新进程,而该新进程的目的是exec一个新程序,比如shell程序。
vfokr和fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中年,因为子进程会立即调用exec(或者exit),也就不会引用该地址空间。不过再在子进程调用exec或exit之前,他在父进程的空间中运行。
并且,vfork保证子进程先行,在子进程调用exec或者exit之后父进程才可能被调度运行,调用任意一个的时候,父进程会恢复运行
#include "apue.h"
int globvar = 6;
int
main(void)
{
int val;
pid_t pid;
var = 88;
printf("before vfork \n");
if((pid = vfork()) < 0)
{
err_sys("vfork error");
}
else if
{
globvar++;
var++;
_exit(0);
}
printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar, var);
exit(0);
}
结果
注意:
调用的是_exit而不是exit。_exit并不执行标准IO缓冲区的冲洗操作。
如果调用exit而不是_exit,则程序的输出是不确定的。它依赖于标准IO库的实现,可能发现没有变化,也有可能发现
没有父进程的printf输出
8.exit函数
正常终止
-1.在main函数执行return语句 等效于调用exit
-2.调用exit函数 包括调用终止处理程序,关闭所有标准IO等
-3.调用_exit或者_Exit 函数。目的是为了为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法。至于对标准IO是否进行冲洗,这取决于实现。_exit函数由exit调用。(在打打杀UNIX系统实现中,exit是标准c库中的一个函数,而_exit是一个系统调用)
-4.进程的最后一个线程在启动历程中执行return语句。但是该线程的返回值不用作于进程的返回值。当最后一个线程从启动历程返回时,该进程以终止状态0返回。
-5.进程的最后一个线程调用 pthread_exit函数。跟4一样
异常终止
-1.调用abort
-2.当进程收到某些信号时
-3.最后一个线对"取消"(cancellation)请求作出响应
不管进程如何终止,都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开的描述符,释放它所使用的存储器。
对于父进程已经终止的所有进程,它们的父进程都会改变为init进程(pid = 1)。大致过程:在一个进程终止时,内核逐个检查所有的活动进程,以判断它是否是正要终止的子进程,如果是,就将该进程的父进程id改为1。
当终止进程的父进程调用wait或者waitpid函数的时候,可以得到终止子进程的信息。包括 进程ID,进程的终止状态,以及该进程使用的cpu时间总量。
内核可以释放终止进程所使用的所有存储区,关闭其所有打开的文件。
僵尸进程:一个已经终止,但是其父进程尚未对其进行善后处理(获取子进程的有关信息,释放它仍占用的资源)的进程被称为僵尸进程。ps命令将僵尸进程的状态打印为Z
一个由init进程收养的进程终止之后会发生什么?它不会变成一个僵尸进程,以为init编写成了无论何时只要一个子进程终止,init就会调用一个wait函数获得其终止状态。
9.wait函数 和 waitpid函数
当一个进程正常或者异常终止时,内核就会向其父进程发送SIGCHLD信号(异步发送)。父进程可以选择忽略,或者提供一个该信号发生时即被调用执行的函数。
当运行这两个函数的时候进程可能发生以下行为:
1.如果所有子进程都还在运行,则阻塞
2.如果一个子进程已经终止,正等待父进程获取其终止状态,则获得该子进程的终止状态立即返回。
3.如果没有任何子进程,则立即出错返回。
#include<sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid,int *statloc,int options);
//成功返回进程ID,出错返回0或者-1
//如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心状态,可以设置为空指针
函数区别:
1.在一个子进程终止前,wait使其调用或者阻塞,而waitpid有一个选项,可以使调用者不堵塞。
2.waitpid并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。
可以根据这两个函数的返回值 调用宏来查看终止状态,从而得知进程终止的原因
waitpid的别的功能
注意:如果指定的进程或者进程组不存在,或者参数pid指定的进程不是调用进程的子进程,都可能出错
总结:
waitpid提供了wait函数没有提供的三个功能:
1.waitpid克等待一个特定的进程,而wait则返回任一终止子进程的状态。
2.waitpid提供了一个wait的非阻塞版本。可以选择阻塞还是非阻塞
3.waitpid通过WUNTRACED和WCONTINUED选项支持作业控制
waitId函数和waitpid相似 : //todo
10.竞争条件:当多个进程企图对共享数据进行处理,而最后的结果取决于进程运行的顺序
11.exec函数
调用fork函数创建新的子进程后,子进程需要调用一种exec函数以执行另外一个程序。当进程调用一种exec函数的时候,该进程执行的程序完全替换为新程序,而新程序则从其main函数上开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段,数据段,堆段和栈段。
1第一个区别:前四个函数取路径名作为参数,后两个取文件名作为参数,最后一个取文件描述符作为参数。
当指定filename作为参数的时候:
1.如果filename包含/ 则视为路径名
2.否则就按PATH环境变量,在指定的各目录中年搜寻可执行文件。
PATH变量包含了一张目录表(路径前缀),目录之间用冒号分割(:)例如:
name=value环境字符串在指定4个目录中年进行搜索
PATH = /bin:/user/bin:/usr/local/bin:.
最后一个.表示当前目录
2第二个区别:参数表的传递(l表示列表list,v表示vector)。函数execl,execlp和execle要求将新程序的每个命令行都要作为一个单独参数。另外四个(execv,execvp,execve,fexecve)则先够着一个指向各个参数的指针数组,然后将该数组地址作为这是个函数的参数
3第三个区别:向新程序传递环境表有关。以e结尾的三个函数(execle,execve,fexecve)可以传递一个指向环境字符串指针数组的指针。另外四个函数则使用调用进程中年的environ变量为新程序复制现有的环境。
例如:在初始化一个新登陆的shell的时候,login程序通常创建一个只定义少数几个变量的特殊环境,而我们在登陆的时候,可以通过shell启动文件,将其他变量加到环境中。
在执行exec后,进程id没有改变,但是新程序从调用进程基础了下列属性
这七个函数的关系:
注意:只有execve是内核的系统调用
exec函数演示
#include "apue.h"
#include <sys/wait.h>
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){
if(execle("/home/sar/bin/echoall" , "echoall" , "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){
if(execlp("echoall", "echoall","only 1 arg", (char *)0 ) < 0)
err_sys("execlp error");
}
exit(0);
}
echoall程序
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);
}