Unix编程 第八章 进程控制

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);
}

结果


image.png

注意结果
如果标准输出连到终端设备,那么它是行缓冲的,否则是全缓冲的
但是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);
}

结果


image.png

注意:
调用的是_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并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。

可以根据这两个函数的返回值 调用宏来查看终止状态,从而得知进程终止的原因

image.png

waitpid的别的功能


image.png

注意:如果指定的进程或者进程组不存在,或者参数pid指定的进程不是调用进程的子进程,都可能出错

image.png

总结:
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只是用磁盘上的一个新程序替换了当前进程的正文段,数据段,堆段和栈段。

image.png

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没有改变,但是新程序从调用进程基础了下列属性


image.png

这七个函数的关系:


image.png

注意:只有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);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容