进程标识与进程创建(pid, fork)

一、进程标识(pid)

  每个进程都有一个非负整数形式的唯一编号,即 PID。PID 在任何时刻都是唯一的,但是可以重用,当进程终止并被回收以后,其 PID 就可以为其它进程所用。进程的 PID 由系统内核根据延迟重用算法生成,以确保新进程的 PID 不同于最近终止进程的 PID。

1、特殊的进程标识

  • 0 号进程,调度进程:通常是调度进程,常常被称为交换进程(swapper)。该进程是内核的一部分,所有进程的根进程,它并不执行任何磁盘上的程序,因此也被称为系统进程。
  • 1 号进程,init进程:通常是 init 进程,在自举过程结束时由内核调用。
  • 2号进程,页守护进程:负责虚拟内存系统的分页操作。

2、获取进程标识

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); 返回:调用进程的进程 ID
pid_t getppid(void); 返回:调用进程的父进程 ID
uid_t getuid(void); 返回:调用进程的实际用户 ID
uid_t geteuid(void); 返回:调用进程的有效用户 ID
gid_t getgid(void); 返回:调用进程的实际组 ID
gid_t getegid(void); 返回:调用进程的有效组 ID
注意,这些函数都没有出错返回

二、进程创建(fork)和父子进程

#include <unistd.h>
pid_t fork(void);

1、函数功能

  fork()主要用于以复制正在调用进程的方式去创建一个新的进程,新进程叫做子进程,原来的进程叫做父进程。

2、与fork()相关的一些问题

(1)由 fork()创建的新进程被称为子进程。fork()函数调用一次,但返回两次。两次返回的区别是:子进程的返回值是 0,而父进程的返回值则是新建子进程的进程 ID通过fork()的返回值来区别父子进程,如果出错返回-1

(2)调用fork()前的代码只有父进程执行,fork()成功返回后的代码,父子进程都会执行。

(3)将新建子进程 ID 返回给父进程的理由:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的 ID。

(4)fork 使子进程得到返回值 0 的理由:一个进程只会有一个父进程,所以子进程总是可以调用 getppid 以获得其父进程的进程 ID(进程 ID 0 总是由内核交换进程使用,所以一个子进程的进程 ID 不可能为 0)。

(5)调用fork()出错的原因:系统中已经存在太多的进程;调用函数fork()的用户进程太多。

3、fork()写时复制

  子进程是父进程的不完全副本。子进程的数据区、bbs区、堆栈区(包括 I/O 流缓冲区),甚至参数和环境区都从父进程拷贝唯有代码区与父进程共享。 因为,代码区是可执行指令 、字面值常量 、具有常属性且被 初始化的全局、静态全局 和 静态局部变量。
  传统的fork()系统调用直接把所有的资源复制给新创建的进程,这种实现过于简单并且效率低下。Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只在需要写入的时候才会复制地址空间,从而使各个进程拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
在这里插入图片描述

4、文件共享

  在重定向父进程的标准输出时,子进程的标准输出也被重定向了。fork的一个特性是进程的所有打开文件描述符都被复制到子进程中。在 fork 之后处理文件描述符有以下两种常用的操作模式:

1. 父进程等待子进程完成:在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已做了相应更新。
2. 父进程和子进程各自执行不同的程序段:在这种情况下,在 fork 之后,父进程和子进程各自关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。

5、父进程和子进程之间的区别

  • fork 的返回值不同,子进程返回 0, 而父进程返回新建子进程 ID。
  • 进程 ID 不同。
  • 这两个进程的父进程 ID 不同:子进程的父进程 ID 是创建它的进程的 ID,而父进程的父进程 ID 则不变。
  • 子进程的tms_utime , tms_stime , tms_cutime以及tms_ustime设置为0。
  • 子进程不继承父进程设置的文件锁。
  • 子进程的未处理闹钟被清除。
  • 子进程的未处理信号集设置为空集。

6、Demo

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

int main()
{
    pid_t pid;
    // fork函数被调用一次,但返回两次;在父进程中返回子进程的pid,子进程返回0
    pid = fork(); // 产生父子进程
    if (pid > 0) // 父进程执行代码段
    {
        while(1)
        {
            printf("I am parent\n");
            printf("my pid = %d",getpid());
            printf("my parent pid = %d",getpid());
            sleep(1);
        }
    }
    else if (pid == 0) // 子进程执行代码段
    {
        while(1)
        {
            printf("I am child\n");
            printf("my pid = %d",getpid());
            printf("my parent pid = %d",getppid());
            sleep(3);
        }
    }
    else
    {
        perror("fork"); //打印出错信息
        exit(1);
    }
    return 0;
}

三、fork(),vfork()和clone()的区别

1、进程四要素

  • 有一段程序供其执行(不一定是一个进程所专有的),就像一场戏必须有自己的剧本。

  • 有自己的专用系统堆栈空间(私有财产)。

  • 有进程控制块(task_struct)(“有身份证,PID”)。

  • 有独立的存储空间。

缺少第四条的称为线程,如果完全没有用户空间称为内核线程,共享用户空间的称为用户线程。

2、fork()

在这里插入图片描述
  fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只在需要写入的时候才会复制地址空间,从而使各个进程拥有各自的地址空间

3、vfork()

  vfork也是创建一个子进程,但是子进程共享父进程的空间在vfork创建子进程之后,父进程阻塞,直到子进程执行了exec()或者exit()。

  vfork创建出来的不是真正意义上的进程,而是一个线程,因为它缺少第四个要素:独立的内存资源

  另外由vfork创建的子进程要先于父进程执行,子进程执行时,父进程处于挂起状态,子进程执行完,唤醒父进程。除非子进程exit或者execve才会唤起父进程。

4、clone()

  clone是Linux为创建线程设计的(虽然也可以用clone创建进程)。所以可以说clone是fork的升级版本,不仅可以创建进程或者线程,还可以指定创建新的命名空间(namespace)、有选择的继承父进程的内存、甚至可以将创建出来的进程变成父进程的兄弟进程等等。

int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);

  clone和fork最大不同在于clone不再复制父进程的栈空间,而是自己创建一个新的。 (void *child_stack)也就是第二个参数,需要分配栈指针的空间大小,所以它不再是继承或者复制,而是全新的创造。

四、孤儿进程与僵尸进程

1、孤儿进程

  父进程创建子进程以后,子进程在操作系统的调度下与其父进程同时运行。如果父进程先于子进程终止,子进程即成为孤儿进程,同时被 init 进程收养,即成为 init 进程的子进程,因此 inti 进程又被成为孤儿院进程。

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

int main (void)
{
    pid_t pid;
    if ((pid = fork ()) < 0)
        perror ("fork"), exit (1);
    else if (pid == 0)
    {
        sleep (3); // 子进程被暂停三秒,所以当父进程退出后子进程仍然未退出。
        printf ("这是子进程 pid = %d", getpid ());
        printf ("父进程的 ppid = %d\n",  getppid ());
    }
    else 
    {
        printf ("这是父进程 ppid = %d\n", getpid ());
    }
    return 0;
}

2、僵尸进程

  如果子进程先于父进程终止,但父进程由于某种原因,没有回收子进程的退出状态,子进程即成为僵尸进程。
  僵尸进程虽然已经不再活动,但其终止状态仍然保留,也会占用系统资源,直到被其父进程回收才得以释放。
  如果父进程直到终止都未回收它的已成僵尸的子进程,init 进程会立即收养并回收这些处于僵尸状态的子进程,因此一个进程不可能既是孤儿进程同时又是僵尸进程。
  一个进程成为僵尸进程需要引起注意,如果它的父进程长期运行而不终止,僵尸进程所占用的资源将长期得不到释放

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

int main (void)
{
    pid_t pid;
    pid = fork ();
    if  (pid == -1)
        perror ("fail to fork"), exit (1);
    else if (pid == 0)
    {
        printf ("这是子进程 pid = %d", getpid ());
        printf ("父进程的 ppid = %d\n",  getppid ());
    }
    else
    {
        // 父进程休眠了十秒,而在这期间,子进程已经退出了,
        //子进程就形成了一段时间的僵尸进程。
        sleep (10); 
        printf ("这是父进程 ppid = %d\n", getpid ());
    }
    return 0;
}

参考:https://blog.csdn.net/qq_29350001/article/details/70229103

  • 7
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~青萍之末~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值