一、进程标识(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。
- 子进程不继承父进程设置的文件锁。
- 子进程的未处理闹钟被清除。
- 子进程的未处理信号集设置为空集。
#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、父进程和子进程的关系
一个父进程可以创建多个子进程,但每个子进程最多只能有一个父进程。整个系统中只有一个根进程,即 PID 为 0 的调度进程。系统中的所有进程构成了一棵以调度进程为根的进程树。
2、父进程和子进程之间的区别
fork 的返回值不同,子进程返回 0, 而父进程返回新建子进程 ID。
进程 ID 不同
这两个进程的父进程 ID 不同:子进程的父进程 ID 是创建它的进程的 ID,而父进程的父进程 ID 则不变。
子进程的tms_utime , tms_stime , tms_cutime以及tms_ustime设置为0。
子进程不继承父进程设置的文件锁。
子进程的未处理闹钟被清除。
子进程的未处理信号集设置为空集。
3、孤儿进程
父进程创建子进程以后,子进程在操作系统的调度下与其父进程同时运行。如果父进程先于子进程终止,子进程即成为孤儿进程,同时被 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;
}
4、僵尸进程
如果子进程先于父进程终止,但父进程由于某种原因,没有回收子进程的退出状态,子进程即成为僵尸进程。
僵尸进程虽然已经不再活动,但其终止状态仍然保留,也会占用系统资源,直到被其父进程回收才得以释放。
如果父进程直到终止都未回收它的已成僵尸的子进程,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;
}
标签:fork,void,编程,pid,Linux,进程,include,ID
From: https://blog.csdn.net/qq_74811378/article/details/142987806