ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

重学计算机(八、进程与创建进程)

2021-12-25 11:01:51  阅读:142  来源: 互联网

标签:fork 00 计算机 00000000 创建 08 pid 进程


在程序是怎么运行中,也讲到进程,但由于篇幅和主题原因,并没有详细介绍;这一次,就要好好介绍一下进程,进程这个概念很多,并且也是操作系统的核心。

8.1 进程

8.1.1 什么是进程

什么是进程?写概念太让人难受了。

我们从第一篇开始,开始写了第一个c语言,然后编译链接,最后生成了一个可执行文件,这个文件叫程序。这个可执行文件里面有什么?可以看这一篇文章重学计算机(三、elf文件布局和符号表),里面包含了各个段,为程序加载做准备。

当我们把这个可执行程序运行起来后,(没有结束之前),它就是一个进程了。

程序是怎么运行的,进程和程序的区别,可以看这一篇:重学计算机(六、程序是怎么运行的)

从专业的角度来讲:进程是操作系统分配资源的基本单位。

进程拥有自己独立的处理环境:环境变量、程序运行目录、进程组等。

进程拥有自己独立的系统资源:处理器CPU的占用率、存储器、I/O设备,数据、程序。

8.1.2 并行和并发

在以前的操作系统中,是存在单道程序设计。

所谓的单道程序设计是所有进程一个一个排队执行,如果A阻塞了,B也只能等待。

相比之下,现在计算机系统允许加载多个程序到内存,以便于并发执行。并发执行其实就是CPU由一个进程快速切换到另一个进程,使每个进程都可以运行一段时间。

在这里插入图片描述

通过这个图就明白了,从时间点来看,CPU只能运行一个程序,但从时间段来看,CPU可以运行多个程序。

正因为需要切换,所以在计算机中时间中断即为进程切换提供了硬件保证。这个下一节再讲。

哪并行是什么呢?

并行是真正的硬件并发,小阳台两个或多个CPU共享同一个物理内存。

在这里插入图片描述

看图业明白了,这是正在的并行执行,互不干扰。

8.1.3 进程的创建

重学计算机(六、程序是怎么运行的)中也提到了fork()。没错,在linux系统下,如果想创建一个进程,就需要调用fork(),fork()是linux的系统API,所有资源都被操作系统管理,当然也包含进程了。(讲到这里是不是也想知道系统调用是怎么调用的?这个我们再讲)

#include <unistd.h>

pid_t fork(void);
/* 功能:
	用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
参数:
	无
返回值:
	成功:子进程中返回0,父进程中返回子进程ID。pid_t为整型
	失败:返回-1。
	失败的两个主要:
	1)当前的进程已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
	2)当系统内存不足,这时errno的值被设置为ENOMEM。
*/

接下来我们就用这个函数来创建第一个子进程,

#include <unistd.h>
#include <stdio.h>
#include <errno.h>

int main(int argc, char **argv)
{
    printf("hello fork\n");

    pid_t pid = fork();
    if(pid < 0)
    {
        printf("fork fail %d\n", errno);
    } 
    else if(pid == 0)       // 这是子进程
    {
        printf("I am son\n");
    } 
    else    // 大于0的为父进程
    {
        printf("parent %d\n", pid);
    } 

    return 0;
}

输出:

root@ubuntu:~/c_test/08# ./fork
hello fork
parent 1524
I am son

fork之后,对于父子进程,哪个先获取CPU资源呢?

在内核2.6.32开始,在默认情况下,父进程将成为fork之后优先调用的对象。采取这种策略的原因:fork之后,父进程在CPU中处于活跃状态,并且其内存管理信息也被置于硬件单元的转译后备缓冲器(TLB),所以优先调度父进程能提升性能。《linux环境编程:从应用到内核》

但是在POSIX标准和linux都没有保证会优先调度父进程。所以在应用中,不能假设父进程先调用,如果需要按顺序调用,需要用到进程同步。

注意:

fork的返回一定需要处理,如果不处理,返回-1,把-1当做进程号,然后调用kill函数的话,kill(-1, 9)会把除了init以外的所有进程都杀死,当然需要权限。

8.1.4 父子进程内存关系

fork之后的子进程完全拷贝了父进程的地址空间,包括了栈、堆、代码段等。

写一段程序来看一下效果:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>


int g_a = 10;       // 全局变量

int main(int argc, char **argv)
{
    int local_b = 20;   // 局部变量
    int *malloc_c = malloc(sizeof(int));

    *malloc_c = 30;     // 堆变量

    pid_t pid = fork();
    if(pid < 0) 
    {
        perror("fork");
        return -1;
    }

    if(pid == 0) {
        // 子进程
        printf("son  g_a:%d p:%p local_b:%d p:%p malloc_c:%d p:%p\n", g_a, &g_a, local_b, &local_b, *malloc_c, malloc_c);
    } else if(pid > 0) {
        // 父进程
        printf("parent  g_a:%d p:%p local_b:%d p:%p malloc_c:%d p:%p\n", g_a, &g_a, local_b, &local_b, *malloc_c, malloc_c);
    }

    if(pid == 0) {
        // 子进程
        g_a = 11;
        local_b = 21;
        *malloc_c = 31;
        printf("son  g_a:%d p:%p local_b:%d p:%p malloc_c:%d p:%p\n", g_a, &g_a, local_b, &local_b, *malloc_c, malloc_c);
    } else if(pid > 0) {
        // 父进程
        sleep(1);
        printf("parent  g_a:%d p:%p local_b:%d p:%p malloc_c:%d p:%p\n", g_a, &g_a, local_b, &local_b, *malloc_c, malloc_c);
    }

    while(1);

    return 0;
}

这里专门定义了3个变量,一个是数据段中的全局变量,一个是栈上的局部变量,一个是堆里的动态变量。

我们写代码,也基本是使用这3中类型的变量,我们编译运行一下:

root@ubuntu:~/c_test/08# ./test_mem
parent  g_a:10 p:0x601060 local_b:20 p:0x7ffe57755668 malloc_c:30 p:0x1aae010
son  g_a:10 p:0x601060 local_b:20 p:0x7ffe57755668 malloc_c:30 p:0x1aae010
son  g_a:11 p:0x601060 local_b:21 p:0x7ffe57755668 malloc_c:31 p:0x1aae010
parent  g_a:10 p:0x601060 local_b:20 p:0x7ffe57755668 malloc_c:30 p:0x1aae010

很明显,前面两行,打印的值都一样,并且虚拟地址都一样,虚拟地址这个内存后面再讲,现在只要去到内存中的值,必须通过虚拟地址映射到物理内存页中,这里指向的哪个物理内存页,我们以后再分析。(感觉又给后面挖坑了)

然后我们在子进程中修改了,3个值,然后继续执行,得出的答案,是父子进程的值不一样了,但虚拟地址还是一样,这个做个标记,以后分析。

下面我们继续查看maps的值:

root@ubuntu:/proc# cat 1522/maps 
00400000-00401000 r-xp 00000000 08:01 11672602                           /root/c_test/08/test_mem
00600000-00601000 r--p 00000000 08:01 11672602                           /root/c_test/08/test_mem
00601000-00602000 rw-p 00001000 08:01 11672602                           /root/c_test/08/test_mem
01aae000-01acf000 rw-p 00000000 00:00 0                                  [heap]
7f6344bba000-7f6344d7a000 r-xp 00000000 08:01 791097                     /lib/x86_64-linux-gnu/libc-2.23.so
7f6344d7a000-7f6344f7a000 ---p 001c0000 08:01 791097                     /lib/x86_64-linux-gnu/libc-2.23.so
7f6344f7a000-7f6344f7e000 r--p 001c0000 08:01 791097                     /lib/x86_64-linux-gnu/libc-2.23.so
7f6344f7e000-7f6344f80000 rw-p 001c4000 08:01 791097                     /lib/x86_64-linux-gnu/libc-2.23.so
7f6344f80000-7f6344f84000 rw-p 00000000 00:00 0 
7f6344f84000-7f6344faa000 r-xp 00000000 08:01 791108                     /lib/x86_64-linux-gnu/ld-2.23.so
7f634519c000-7f634519f000 rw-p 00000000 00:00 0 
7f63451a9000-7f63451aa000 r--p 00025000 08:01 791108                     /lib/x86_64-linux-gnu/ld-2.23.so
7f63451aa000-7f63451ab000 rw-p 00026000 08:01 791108                     /lib/x86_64-linux-gnu/ld-2.23.so
7f63451ab000-7f63451ac000 rw-p 00000000 00:00 0 
7ffe57737000-7ffe57758000 rw-p 00000000 00:00 0                          [stack]
7ffe57794000-7ffe57797000 r--p 00000000 00:00 0                          [vvar]
7ffe57797000-7ffe57799000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
root@ubuntu:/proc# cat 1523/maps 
00400000-00401000 r-xp 00000000 08:01 11672602                           /root/c_test/08/test_mem
00600000-00601000 r--p 00000000 08:01 11672602                           /root/c_test/08/test_mem
00601000-00602000 rw-p 00001000 08:01 11672602                           /root/c_test/08/test_mem
01aae000-01acf000 rw-p 00000000 00:00 0                                  [heap]
7f6344bba000-7f6344d7a000 r-xp 00000000 08:01 791097                     /lib/x86_64-linux-gnu/libc-2.23.so
7f6344d7a000-7f6344f7a000 ---p 001c0000 08:01 791097                     /lib/x86_64-linux-gnu/libc-2.23.so
7f6344f7a000-7f6344f7e000 r--p 001c0000 08:01 791097                     /lib/x86_64-linux-gnu/libc-2.23.so
7f6344f7e000-7f6344f80000 rw-p 001c4000 08:01 791097                     /lib/x86_64-linux-gnu/libc-2.23.so
7f6344f80000-7f6344f84000 rw-p 00000000 00:00 0 
7f6344f84000-7f6344faa000 r-xp 00000000 08:01 791108                     /lib/x86_64-linux-gnu/ld-2.23.so
7f634519c000-7f634519f000 rw-p 00000000 00:00 0 
7f63451a9000-7f63451aa000 r--p 00025000 08:01 791108                     /lib/x86_64-linux-gnu/ld-2.23.so
7f63451aa000-7f63451ab000 rw-p 00026000 08:01 791108                     /lib/x86_64-linux-gnu/ld-2.23.so
7f63451ab000-7f63451ac000 rw-p 00000000 00:00 0 
7ffe57737000-7ffe57758000 rw-p 00000000 00:00 0                          [stack]
7ffe57794000-7ffe57797000 r--p 00000000 00:00 0                          [vvar]
7ffe57797000-7ffe57799000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

仔细观察,是不是每个段的内存地址值都一样,并且各个段的内存都一样。

是这样的,那我们从这里是不是可以推测出fork对内存的操作呢?

在传统Unix系统中:子进程复制父进程的所有资源,包含进程的地址空间,包括进程的上下文(进程执行活动全过程的静态描述)、进程的堆栈等。

看到传统两字了,这种玩法就肯定有缺点,缺点如下:

  1. 使用大量内存
  2. 复制操作也耗费很大时间,导致fork效率很低
  3. 通常情况下,我们会调用exec函数,执行另一个进程,而不会在这个父进程中执行,这样导致大量的复制都在无用功。

所以linux现在使用了写时拷贝(copy-on-write)的技术,这种技术也挺好理解的,在fork过程中,子进程并不需要完全复制父进程的地址空间,而是让父子进程共享同一个地址空间,并且把这些地址空间设置为只读。当父子进程其中有一方尝试修改,就会引发缺页异常,然后内核就会尝试为该页面创建一个新的物理页,并将真正的值写到新的物理页中,这样就是写时拷贝,毕竟靠谱的一个技术。

是不是感觉写到这里就结束了?在仔细看看代码,我们malloc了变量,还没有释放呢?

这里就有一个问题,怎么释放?加入了子进程后,释放问题是如何的?

通过上面的分析,子进程会拷贝一份堆空间,所以说子进程的堆里也是有一个malloc_c的指针的,所以在这种情况,malloc是一次申请,需要两次释放(分别是父子进程)。

大家可以去试试。

8.1.5 父子进程文件关系

执行fork函数,内核会复制父进程所有的文件描述符。所以子进程也是可以操作父进程的所打开的文件。

下面我们来写个代码来测试一下,父子进程文件的关系。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>


#define     INFILE      "./in.txt"
#define     OUTFILE     "./out.txt"


int main(int argc, char**argv)
{
    // 先打开文件
    int r_fd = open(INFILE, O_RDONLY);
    if(r_fd < 0)
    {
        printf("open %s\n", INFILE);
        return 0;
    }

    int w_fd = open(OUTFILE, O_WRONLY | O_CREAT | O_TRUNC);
    if(w_fd < 0) 
    {
        printf("open %s\n", OUTFILE);
        return 0;
    }


    // 创建子进程
    pid_t pid = fork();
    if(pid < 0)
    {
        printf("fork error\n");
        return 0;
    } 
    
    char buf[100];
    memset(buf, 0, 100);

    // 父子进程一样,读文件,再写文件
    while(read(r_fd, buf, 2) > 0)
    {
        printf("pid:%d buf:%s\n", getpid(), buf);
        sprintf(buf, "pid:%d \n", getpid());
        write(w_fd, buf, strlen(buf));                      // 多个进程操作一个w_fd
        sleep(1);
        memset(buf, 0, 100);
    }


    while(1);
    close(r_fd);
    close(w_fd);

    return 0;
}

我们来看一下代码执行的效果:

root@ubuntu:~/c_test/08# ./test_file
pid:1501 buf:1

pid:1502 buf:2

pid:1501 buf:3

pid:1502 buf:4

pid:1502 buf:5
pid:1501 buf:6

通过这个输出发现,父子进程读取共享文件的指针偏移是一个,所以可以顺序读取,如果不是一个,父子进程读取都是从1-6.

root@ubuntu:~/c_test/08# cat out.txt 
pid:1501 
pid:1502 
pid:1501 
pid:1502 
pid:1501 
pid:1502

写文件的时候也是共享一个文件指针,所以才是交替写入。

如果这样子是不是不太安全,那子进程怎么才能不访问到父进程的共享文件。

其实open函数是有一个标志的:O_CLOSEXEC。

这个一看名字就知道了,在执行exec函数之后,会把共享文件关闭,这样子进程就不能访问到父进程打开的文件了。

8.1.6 vfork()

早期没有fork的写时复制的时候,用fork创建进程,是真的慢,所以大佬们创建了一个新的创建进程的函数vfork()。

vfork()的实现:不会拷贝父进程的内存数据,直接共享。

这样共享会不会有问题,当然会了,只不过这个vfork()会保证子进程先运行,并且父进程先挂起,直到子进程调用了_exit、exit或者exec函数之后,父进程再接着运行。

不过这个vfork在fork出现了写时复制的时候,已经被淘汰了,这里就不写例子了,淘汰的函数,也没有必要使用了。

8.1.7 进程树

既然所有的进程都是从父进程fork过来的,那总是有一个祖宗进程,这个祖宗进程就是系统启动的init进程:

在这里插入图片描述

这图出自刘超老师的 趣谈操作系统。

这个图,我们下一节讲,哈哈哈。

附:

子进程继承了父进程的属性:

  1. 整个内存部分。(写时拷贝)
  2. 打开文件的偏移指针
  3. 实际用户ID、实际组ID、有效用户ID、有效组ID
  4. 附加组ID、进程组ID、会话组ID、
  5. 控制终端
  6. 设置用户ID标志和设置组ID标志
  7. 当前工作目录
  8. 根目录
  9. 文件模式创建屏蔽字
  10. 信号屏蔽和安排
  11. 针对任一打开文件描述符的在执行时关闭标记
  12. 环境
  13. 连接的共享存储段
  14. 存储映射
  15. 资源限制

父子进程之间的区别:

  1. fork的返回值
  2. 进程ID不同
  3. 两个进程具有不同的父进程ID
  4. 子进程的tms_utime、tms_stime、tms_cutime以及tms_ustime均被设置为0
  5. 父进程设置的文件锁不会被子进程继承
  6. 子进程的未处理的闹钟被清楚
  7. 子进程的未处理信号集设置为空集

真是太多属性了,好多都不是很清楚,慢慢看吧,加油。

标签:fork,00,计算机,00000000,创建,08,pid,进程
来源: https://blog.csdn.net/C1033177205/article/details/121300326

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有