进程的创建

《Linux内核设计与实现》笔记系列一

进程创建

许多其他的操作系统都提供了产生(spawn)进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。

unix中才用把上述步骤分解为两步:

  1. fork( ),通过拷贝当前进程创建一个子进程。
  2. exec( ),负责读取可执行文件并将其载入到地址空间开始运行。

写时拷贝(copy-on-write COW)

Linux中的fork()使用了copy-on-write页实现。

COW只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。

具体来说:

​ 当进程A使用系统调用fork()创建子进程B时,由于子进程B实际上是父进程A的一个拷贝.因此会拥有与父进程相同的物理页面.为了节约内存和加快进程的创建速度,fork()会让子进程以只读的方式共享父进程A的物理页面,同时也将父进程A对这些物理页面的访问权限设置为只读.这样,当父进程A或子进程B任何一方对这些已共享的物理页面执行写操作时,都会产生页面出错异常(page_fault)中断,此时CPU会执行系统提供的异常处理函数do_wp_page()来解决这个异常.

do_wp_page()会对这块导致写入异常中断的物理页面进行取消共享操作,为写进程复制一份新的物理页面,使得进程A与进程B各自拥有一块相同的物理页面,最后从异常处理函数中返回时,CPU就会重新执行刚才导致异常的写入操作指令,使进程继续下去.

fork()的实际开销主要是复制父进程的页表以及子进程创建唯一的task_struct(进程描述符).在一般情况下,进程创建后会马上运行一个可执行文件,这种优化可以避免拷贝根本不会被使用的数据.

fork()、vfork()和clone() 之间的关系

系统调用 描述
fork() fork创造的子进程是父进程的完整副本,复制了父亲进程的资源,包括内存的内容task_struct内容
vfork() vfork创建的子进程与父进程共享数据段,而且由vfork()创建的子进程将先于父进程运行
clone() linux上创建线程一般使用的是pthread库 实际上linux也给我们提供了创建线程的系统调用,就是clone

fork

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>


int main(){
int cnt = 1;
int child = fork();
if (child < 0)
printf("fork() error!\n");
else if (child == 0){
// child
printf("child PID = %d, cnt = %d\n", getpid(), cnt);
}
else{
// parent
cnt ++;
printf("parent PID = %d, cnt = %d\n", getpid(), cnt);
}
sleep(1);
return 0;
}

从运行结果可以看出父子进程的PID不同,堆栈和数据资源都是完全相同。

当父进程改变cnt的值并不会影响子进程中的cnt的值。

vfork

vfork()的做法较之fork()更加粗暴,内核连子进程的虚拟地址空间结构也不创建,直接共享了父进程的虚拟地址空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>


int main(){
int cnt = 1;
int child = vfork();
if (child < 0)
printf("fork() error!\n");
else if (child == 0){
// child
cnt ++;
printf("child PID = %d, cnt = %d\n", getpid(), cnt);
exit(0);
}
else{
cnt ++;
printf("parent PID = %d, cnt = %d\n", getpid(), cnt);
}
sleep(1);
return 0;
}

vfork()保证了子进程先运行(父进程被挂起),再子进程调用exec_exit后父进程才有可能被调度.

从运行结果可以看出: 子进程对cnt加一,父进程中的cnt也加一,说明两者的cnt在同一内存.

另外在vfork()中退出一定要使用_exit()而不要使用return.

vfork()时父子进程共享数据段,如果vfork()的子进程中使用return,父进程去访问数据将会出现段错误.

这是因为如果vfork()生成的子进程在函数中使用return就结束了函数,其函数的栈空间全部被系统回收,父进程再去访问原来的数据就访问错误的地址,导致段错误.

exit 与 return 的区别

exit(0):正常运行程序并退出程序;

exit(1):非正常运行导致退出程序;

return():返回函数,若在主函数中,则会退出函数并返回一个值。]

return是语言级别的,它表示了调用堆栈的返回;而exit是系统调用级别的,它表示了一个进程的结束

return是函数的退出(返回);exit是进程的退出。

return用于结束一个函数的执行,将函数的执行信息传出个其他调用函数使用;exit函数是退出应用程序,删除进程使用的内存空间,并将应用程序的一个状态返回给OS,这个状态标识了应用程序的一些运行信息,这个信息和机器和操作系统有关,一般是 0 为正常退出,非0 为非正常退出。

非主函数中调用return和exit效果很明显,但是在main函数中调用return和exit的现象就很模糊,多数情况下现象都是一致的。

linux中的线程

linux中把所有的线程都当做进程来实现,内核并没有特别的调度算法或是定义特别的数据结构来表征线程.相反,线程仅仅是被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属自己的task_struct,所以在内核中,它看起来就像一个普通的进程。(只是线程和其他一些进程共享某些资源)

对于Linux来说,线程只是进程间的一种共享资源的手段(linux中的进程本身已经够轻量级了)

linux中线程的实现方式与进程一样,只不过通过对clone()参数指定来使得多个进程之间共享某些资源(如虚拟内存、页表、文件描述符),函数调用栈、寄存器等线程私有数据则独立。

1
2
3
4
5
6
// 创建线程
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
// fork()的实现
clone(SIGCHLD, 0);
// vfork()的实现
clone(CLONE_VFORK| CLONE_VM | SIGCHLD, 0);
进程与线程

linux中进程和线程实际上都是使用一个结构体task_struct来表示一个执行任务的实体。进程创建调用fork()系统调用,而线程创建则是pthread_create()方法,但是这两个方法最终都会调用do_fork()函数来创建操作,区别就送在传入的参数不同。

参考链接:

https://blog.csdn.net/gatieme/article/details/51417488