栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

APUE---chap8(进程控制)---8.2~8.10(getpid/fork/wait/waitpid/exec)

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

APUE---chap8(进程控制)---8.2~8.10(getpid/fork/wait/waitpid/exec)

8.2 进程标识

每一个进程都有一个非负的唯一进程ID,且这个ID是可以复用的。

系统中有专有的进程:

ID为0:调度进程,也称交换进程,也叫系统进程ID为1:init进程,通常位于/sbin目录下. init通常读取与系统的初始化文件(/etc/rc*或etc/inittab文件,及在/etc/init.d中的文件),并引导系统到一个状态. init进程绝不会终止,它是一个普通用户进程,但以超级用户特权运行.. 

各种ID可通过下面的接口读取

8.3函数fork
pid_t ret = fork(void);

1. 调用会创建一个一样的子进程

2.失败 ret < 0 ;  父进程 ret > 0;   子进程 ret = 0; 

使用Demo

#include 
#include 
#include 
#include 

int main()
{
    pid_t pid;
    printf("Begin %dn", getpid());
    
    pid_t tmp = fork();
    if(tmp < 0) { // fail
        perror("fail");
        exit(1);
    }
    if (tmp == 0) { //child
        printf("child is working %dn", getpid());
    }
    else {  //parent
        // sleep(2);
        printf("parent is working %dn", getpid());
    }
    printf("End %dn", getpid());
    exit(0);
}

结果:

zion6135@zion6135-VirtualBox:~/Desktop$ ./a.out
Begin 31828
parent is working 31828
End 31828
zion6135@zion6135-VirtualBox:~/Desktop$ child is working 31829
End 31829
ls
a.out test.cpp

zion6135@zion6135-VirtualBox:ps axf
  31874 pts/1    S+     0:00  |           |   _ ./a.out
  31875 pts/1    S+     0:00  |           |       _ ./a.out

注:子进程,父进程的运行先后不是绝对的, 是由调度策略来决定谁先被调用,  如果想让child先运行可以让parent先sleep(2);来让子进程先运行.(但sleep这种方法是有问题的,不能这么用. )

重映射打印两次Begin

zion6135@zion6135-VirtualBox:~/Desktop$ ./a.out > ~/Desktop/tmp
#打印两次Begin
zion6135@zion6135-VirtualBox:~/Desktop$ cat tmp 
Begin 31943
parent is working 31943
End 31943
Begin 31943
child is working 31944
End 31944
zion6135@zion6135-VirtualBox:

为什么会终端和重映射打印结果不一样呢?

因为终端是标准输出设备, 默认是行缓冲模式,所以终端中的打印语句有'n',会刷新缓存.

而重映射到文件,默认是全缓冲模式,所以不管有没有'n'都不会刷新缓存,

当调用fork()的时候, 父子进程都有一句缓冲的 printf("Begin %dn");的语句, 所以子进程父进程的结果都是Begin 31943所以需要在fork()之前加上fflush(NULL)来刷新缓冲区.来解决这个问题. 

[注]  说明fork()之后,父子进程的缓冲也被复制了. 

fork实例2:质数计算求取,通过fork来计算一个数:说明fork有时候并不能提高程序速度

// 质数计算求取,通过fork来计算一次
#if 1
#include 
#include 
#include 
#include 
#define LEFT 30000000
#define RIGHT 30000200

int main() {
    int mark, j;
    for (int i = LEFT; i <= RIGHT; i++) {
        pid_t pid = fork();  // 父-->子-->孙-->。。。共200次
        if (pid < 0) {
            perror("error");
            exit(1);
        }
        if (pid == 0) {
            mark = 1;
            for (j = 2; j < i / 2; j++) {
                if (i % j == 0) {
                    mark = 0;
                    break;
                }
            }
            if (mark) {
                printf("%d is a primern", i);
            }
            exit(0);  //必须退出子进程--否则程序会fork 200次
        }
    }
    exit(0);
}

结果:

zion6135@zion6135-VirtualBox$./primer
zion6135@zion6135-VirtualBox$ 30000083 is a primer
30000149 is a primer
30000109 is a primer
30000041 is a primer
30000023 is a primer
30000037 is a primer
30000001 is a primer
30000169 is a primer
30000137 is a primer
30000133 is a primer
30000071 is a primer
30000163 is a primer
30000193 is a primer
30000199 is a primer
30000049 is a primer
30000059 is a primer
30000167 is a primer
30000079 is a primer

按理说,200个子线程,相比无fork的一个程序的计算,200个子线程并不一定会更快,因为子线程的切换需要任务调度,开销会更多。

查看程序状态:man ps查看程序状态种类

8.4 程序vfork

之前:

比如parent process有30万条数据,如果希望子进程打印hello world, 那么调用fork()会让子进程复制30万条数据,极大的浪费了资源。

如果用vfork()产生一个子进程,那么子进程和父进程共用一个内存。查看手册:有这么一句话       vfork() was called, or calls any other function before successfully calling _exit(2) or one  of  the exec(3) family of functions. 这表明在调用vfork()生成的子进程中,只能调用上面的2个函数:exex() 和 _exit(2);

所以如果有这么个问题:父进程打开一个文件描述符,虽然公用一片数据块,但是vfork的子进程是不可以关闭这个文件描述符的。

现在:

fork是父进程,子进程共用一份内存,如果父或子进程需要修改某个数据,那么就会自动的拷贝这一份数据:称为写时拷贝技术。所以现在不怎么需要vfork()函数了. 

8.6 wait()和waitpid()

1.wait()

当一个进程正常或者异常终止,就会向它的父进程发送一个SIGCHLD信号。

wait(&status);  // 在调用wait后的第一个子进程终止之前,wait() 使其调用者阻塞。

                        // 等同于waitpid(-1, &status, 0);

wait函数的使用的Demo

#include 
#include 
#include 
#include 
#include 
#define LEFT 30000000
#define RIGHT 30000200

int main() {
    int mark, j;
    for (int i = LEFT; i <= RIGHT; i++) {
        pid_t pid = fork();  // 父-->子-->孙-->。。。共200次
        if (pid < 0) {
            perror("error");
            exit(1);
        }
        if (pid == 0) {
            mark = 1;
            for (j = 2; j < i / 2; j++) {
                if (i % j == 0) {
                    mark = 0;
                    break;
                }
            }
            if (mark) {
                printf("%d is a primern", i);
            }
            exit(0);  //必须退出子进程--否则程序会fork 200次
        }
    }
    // 等子进程结束,主线程为其收尸
    for (int i = LEFT; i <= RIGHT; i++) {
        wait(NULL);  //阻塞等待每个子进程exit(0); 退出
    };

    exit(0);
}

结果:

#调用wait的本次打印结果
zion6135@zion6135:~/lbw/gitNote/chap8$ ./a.out 
30000001 is a primer
...
zion6135@zion6135:~/lbw/gitNote/chap8$ 


#未调用wait的打印结果
zion6135@zion6135:$ ./a.out
zion6135@zion6135:$ 30000083 is a primer
30000149 is a primer
...
zion6135@zion6135:$ 

2. waitpid();

pid_t waitpid(pid_t pid, int *wstatus, int options);   // 可以控制等待的方式。使其不阻塞。

8.10 函数exec

比如在一个终端bash里面执行一个程序a.out ; 终端bash是aout的父进程,但与调用fork()生成的父子进程不同, 终端bash的子进程是用的exec生成,也是在终端执行 ./a.out这个动作。比如 执行 ./a.out first ---对应到execl函数就是execl(a.out, 1, first);

execl的简单使用+用fflush(null)避免重定向的丢失日志puts("Begin")

#include 
#include 
#include 

int main() {
    puts("beginn");
    // fflush(NULL);
    int err = execl("/bin/date", "date", "+%s", NULL);
    perror("execl");

    puts("endn");
    exit(0);
}

打印结果:

abc@123:~/lbw/gitNote/chap8$ ./main 
begin

1645097863
abc@123:~/lbw/gitNote/chap8$ ./main > tmp
abc@123:~/lbw/gitNote/chap8$ cat tmp
1645097919

重定向到tmp文件,没有了begin的打印,原因和之前一样,因为execl()函数是替换可执行程序的image, puts("beginn");是行缓冲,如果用execl()函数了,默认变成了全缓冲,所以n不带刷新缓冲的作用了,所以tmp文件中没有Begin的打印日志。所以为了把调用execl()之前的打印放入tmp文件,需要调用fflush(NULL)来刷新缓冲。来让Begin可以重定向到文件tmp中。

虽然execl()函数是替换可执行程序的image,但是程序的pid是没变的。

fork+execl+wait的使用示例:简单的shell模型

#include 
#include 
#include 

int main() {
    puts("beginn");
    fflush(NULL);  //避免重定向不能保存begin的日志

    pid_t pid = fork();  // 父子进程
    if (pid < 0) {
        perror("fork()");
        exit(1);
    }
    if (pid == 0) {
        execl("/bin/date", "date", "+%s", NULL);  // 子进程从date文件的main开始执行
        perror("execl()");
        exit(1);
    }
    wait(NULL);  //为子进程收尸

    puts("endn");
    exit(0);
}

结果:

123@abc:~/lbw/gitNote/chap8$ ./main
begin

1645098780
end

123@abc:~/lbw/gitNote/chap8$ 

比如shell终端的使用例子(shell工作模式)

在shell终端输入ls命令,shell会先fork一个子进程,然后在主进程调用wait(NULL); 等待子进程结束,子进程就取调用execl(“/bin/ls”, "ls" , "%s", null); 去执行可执行文件ls ,然后打印出当前目录下所有文件,子进程结束。主进程的wait(NULL)执行结束。继续打印新的一行!!!!!!

上面讲的过程就是上面写的例子:只不过把父进程换成了shell终端,把执行的文件换成了"ls",而不是"date"......

在比如8.2 fork当中的fork实例2:可以解释为什么结果是先打印了“zion6135@zion6135-VirtualBox$" 后打印了”30000083 is a primer”   

因为./primer执行后,shell创建了子进程./primer,./primer fork()结束后,./primer进程在main()函数最后一行调用exit(0)后,shell为其子进程./primer收尸,然后shell打印"zion6135@zion6135-VirtualBox$" , 这个时候./primer的子进程(shell的孙子进程)变成了孤儿进程,继续跑。于是打印了”30000083 is a primer”   

为什么父子进程会通用一个shell, 即打印在一个终端里面?

因为父进程fork(),会把自己的文件描述符表复制(dumplicate)给子进程,所以父进程在哪里打印,子进程就在哪里打印。

shell的外部命令简单实现:

外部命令(磁盘上有的程序,ls, cat 等等)

#include 
#include 
#include 
#include 
#include 

#define DELINITS " tn"  // 三种分割符:空格,tab,换行

struct cmd_st {
    glob_t globres;
};

static void promt() {
    printf("mysh-0.1$");
}

static void prase(const char *buffer, struct cmd_st *cmd) {
    //解析一行的输入: 比如 ls -a -l /etc    /aaa/bbbb

    char *tok;

    int i = 0;  //第一次不用APPEND, 第二次在APEEND
    while (1) {
        tok = strsep(&buffer, DELINITS);
        if (tok == NULL) {
            break;
        }
        if (tok[0] == '')  //多个空格
            continue;
        glob(tok, GLOB_NOCHECK | GLOB_APPEND * i, NULL, &cmd->globres);
        i = 1;
    }
}
int main() {
    puts("beginn");

    while (1) {
        promt();  //打印提示符
        char *linebuffer = NULL;
        size_t line_size = 0;
        struct cmd_st cmd;

        //拿到每一行的输入
        if (getline(&linebuffer, &line_size, stdin) < 0) {
            break;
        }
        prase(linebuffer, &cmd);  //解析命令
        if (0) {
            //内部命令
        } else {
            //外部命令
            pid_t pid = fork();
            if (pid < 0) {
                perror("fork()");
                exit(1);
            }
            if (pid == 0) {
                execvp(cmd.globres.gl_pathv[0], cmd.globres.gl_pathv);
                perror("execvp()");
                exit(1);
            } else {
                wait(NULL);
            }
        }
    }
    puts("endn");
    exit(0);
}

结果:

123@abc:~/lbw/gitNote/chap8$ ./main
begin

mysh-0.1$ls
a.out  main  main.c  tmp
mysh-0.1$

修改/etc/passwd 文件把/bash/bin 改为你写的shell的程序路径,然后切ctrl+alt+f1切换到命令行界面,然后登陆进去,会发现:执行的shell是刚刚写好的shell!!!!!!

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/746750.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号