进程:操作系统资源分配的基本单位;线程:任务调度和执行的基本单位
1.区别
- 1、每段进程都有独立的空间,但是同一类线程共享进程的代码和数据空间,其所使用的资源来自进程。进程和线程最大的区别就在于有没有独立的存储空间。
-
2、操作系统可以同时运行多个进程,但是单个cpu同一时刻只能运行一个进程。同一个进程中多个线程是通过cpu调度的方式(时间片)进行执行的
自己写一个案例(fork)
fork
上图展示了fork创建一个进程的过程,其中getpid()为获得当前进程的pid号。正常的程序只有一个返回值,但是在该函数中,返回了两个结果。进入main()的进程为父进程,在第9行的时候,pid 系统调用了fork(),此时一个新的子进程诞生了,这个子进程全面复制了父进程的所有资源和特性,此刻有两个长得一样的main进程出现了。本来只打印一次的程序,出现了打印两次的情况。
那么如何知道此刻运行的进程到底是父进程还是子进程呢?通过fork()的返回值可以判断。当pid等于0时,表明该进程为子进程,当pid不等于0时,该进程为父进程。从输出的结果可以看到,当进程为父进程的时候,fork的返回值为子进程的pid号。
2.Bionic中的fork()
在bionic目录下的fork.cpp中可以看出,fork的实现调用了clone函数,而clone函数的实现调用了__bionic_clone.S,不同环境的__bionic__clone.S在真正执行clone时,无论在x86,arm还是在riscv的汇编代码中都是采用的syscall系统调用,去内核中执行clone



3.进程描述符(task_struct)
操作系统想要管理进程,实则是通过将进程的有效信息提取出来进行管理的,进程的相关信息则被存放在一个叫做进程控制块的数据结构中。task_struct是Linux内核的一种数据结构,它会被装载到RAM中并且包含着进程的信息。每个进程都把它的信息放在 task_struct 这个数据结构体。该结构体位于inclue/linux/sched.h中。在内核中,定义了一个current.h函数在提供指向task_struct结构的指针。

接下来介绍一下task_struct中几个比较重要的参数:
3.1 stage
state表示当前进程的运行状态
/* Used in tsk->state: */
#define TASK_RUNNING 0x0000 //进程正在执行或者准备执行
#define TASK_INTERRUPTIBLE 0x0001 //表示进程处于睡眠状态
#define TASK_UNINTERRUPTIBLE 0x0002 //表示进程处于睡眠状态
#define __TASK_STOPPED 0x0004 //主要用于调试,接收SIGSTOP挂起,接收SIGCONT命令继续恢复执行
#define __TASK_TRACED 0x0008 //进程被追踪
/* Used in tsk->exit_state: */
#define EXIT_DEAD 0x0010 //进程的最终状态,进程死亡
#define EXIT_ZOMBIE 0x0020 //僵尸状态的进程

3.2 Flag
Flag也是反应进程状态的信息,但是不是在运行状态,而是在管理有关的其他信息。
#define PF_IDLE 0x00000002 /* I am an IDLE thread */
#define PF_EXITING 0x00000004 /* Getting shut down */
#define PF_EXITPIDONE 0x00000008 /* PI exit done on shut down */
#define PF_VCPU 0x00000010 /* I'm a virtual CPU */
#define PF_WQ_WORKER 0x00000020 /* I'm a workqueue worker */
#define PF_FORKNOEXEC 0x00000040 /* Forked but didn't exec */
#define PF_MCE_PROCESS 0x00000080 /* Process policy on mce errors */
#define PF_SUPERPRIV 0x00000100 /* Used super-user privileges */
#define PF_DUMPCORE 0x00000200 /* Dumped core */
#define PF_SIGNALED 0x00000400 /* Killed by a signal */
#define PF_MEMALLOC 0x00000800 /* Allocating memory */
#define PF_NPROC_EXCEEDED 0x00001000 /* set_user() noticed that RLIMIT_NPROC was exceeded */
#define PF_USED_MATH 0x00002000 /* If unset the fpu must be initialized before use */
#define PF_USED_ASYNC 0x00004000 /* Used async_schedule*(), used by module init */
#define PF_NOFREEZE 0x00008000 /* This thread should not be frozen */
#define PF_FROZEN 0x00010000 /* Frozen for system suspend */
#define PF_KSWAPD 0x00020000 /* I am kswapd */
#define PF_MEMALLOC_NOFS 0x00040000 /* All allocation requests will inherit GFP_NOFS */
#define PF_MEMALLOC_NOIO 0x00080000 /* All allocation requests will inherit GFP_NOIO */
#define PF_LESS_THROTTLE 0x00100000 /* Throttle me less: I clean memory */
#define PF_KTHREAD 0x00200000 /* I am a kernel thread */
#define PF_RANDOMIZE 0x00400000 /* Randomize virtual address space */
#define PF_SWAPWRITE 0x00800000 /* Allowed to write to swap */
#define PF_MEMSTALL 0x01000000 /* Stalled due to lack of memory */
#define PF_NO_SETAFFINITY 0x04000000 /* Userland is not allowed to meddle with cpus_allowed */
#define PF_MCE_EARLY 0x08000000 /* Early kill for mce process policy */
#define PF_MUTEX_TESTER 0x20000000 /* Thread belongs to the rt mutex tester */
#define PF_FREEZER_SKIP 0x40000000 /* Freezer should not count it as freezable */
#define PF_SUSPEND_TASK 0x80000000 /* This thread called freeze_processes() and should not be frozen */
3.3 Pid
进程的唯一标识(pid)
pid_t pid; //进程标识符
pid_t tgid; //线程组中thread group leader的pid
3.4 进程间关系
这部分用来描述进程之间的亲属关系
struct task_struct __rcu *real_parent; //指向其父进程,如果创建它的父进程不在,指向PID为1的init进程
/* Recipient of SIGCHLD, wait4() reports: */
struct task_struct __rcu *parent; //其值通常与real_parent的值相同
/*
* Children/sibling form the list of natural children:
*/
struct list_head children; //标识链表的头部,链表中元素都是进程的子进程
struct list_head sibling; //用于把当前进程插入到兄弟链表
struct task_struct *group_leader; //指向其进程组所在的头部进程
3.5 进程优先级与调度信息
期限进程的优先级是-1,实时优先级的范围是,1到MAX_PT_PRIO-1(99),普通进程的静态优先级是从MAX_PT_PRIO-1到MAX_PRIO-1(即100到139),值越大静态优先级越低。
int prio; //用于保存动态优先级
int static_prio; //用于保存静态优先级
int normal_prio; //取决于静态优先级和调度策略
unsigned int rt_priority; //用于保存实时优先级
unsigned int policy; //用于表示进程的调度策略
关于policy进程的调度策略,定义的调度方式位于include/uapi/linux/sched.h中,主要有以下五种:
#define SCHED_NORMAL 0 //按照优先级进行调度
#define SCHED_FIFO 1 //先进先出的调度算法,在没有更高优先级的实时进程时,将一直霸占cpu
#define SCHED_RR 2 //时间片轮转的调度算法,进程用完cpu之后加入优先级对应运行队列的尾部,把处理器让给其他优先级相同的其他实时进程
#define SCHED_BATCH 3 //批量调度策略
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE 5 //空闲调度,用来执行优先级非常低的后台作业
#define SCHED_DEADLINE 6 //限期进程使用限期调度策略,每个周期运行一次,在截至期限之前执行完
3.6 进程链表
进程链表就是把进程的描述符链接起来,每个task_struct都包含一个list_head类型的tasks字段,这个类型的prev和next字段分别指向前面和后面和task_struct元素。进程链表的头是init_task的描述符,是0号进程的描述符。
由于进程号分配是非常随机的,其号并不包含任何可以用来找到进程的路径信息,寻找进程首先是利用了遍历进程链表办法,但是顺序扫描进程链表并检查进程描述符中的pid字段是比较低效的。为了迅速找到进程,linux内核引入了四个散列表(pidhash),代表了不同类型的pid字段.
4 进程的创建
Linux将进程的创建和目标程序的执行分为两步,第一步就是复制一个和父进程一样的子进程,此时的子进程拥有自己的task_struct(进程描述符)和系统空间堆栈,共享与父进程的所有其他资源。比如父进程此时打开了五个文件,子进程此时也打开了五个文件,这些文件的指针与父进程一样停留在相同的地方,这一步就是复制。所谓复制就是对基本资源的复制,比如task_struct数据结构,系统空间堆栈,页面表等,但是对父进程的代码和全局变量则不复制,而是通过只读访问的形式实现共享。此时的复制就是说,父进程给子进程创建了一个新的虚拟地址,但是此时的虚拟地址指向的还是父进程的物理空间,并没有给子进程分配新的物理空间。做一个比喻就是,此时的进程是个图书馆,里面有很多书,也占据了很多地方。Fork第一步是先把这些书的名字和信息给复制下来,万一你以后不看,我把这个图书馆复制下来岂不是浪费了很多的地方?等哪天需要真的看这些书的时候,再执行第二步,把整个图书馆给你复原一下。此时的子进程还没有自己的物理空间,如果后面的子进程并没有用到写,子进程就还是一个线程。
Fork的第二步就是目标程序的执行,当子进程需要写的时候,系统调用execve()。
4.1 Fork,vfork和clone
Fork:子进程是父进程的副本,采用了写时复制的技术。
Vfork:创建子进程后立即调用execve进行程序的执行,但是为了避免复制物理页,父进程会进入睡眠等待子进程装在新程序。随着fork写时复制技术的使用,vfork已经被淘汰。
Clone:可以控制子进程和父进程共享那些资源,可以通过clone创建一个进程,也可以通过pthread创建线程。其实fork,vfork和clone这三个在内核中都是基于函数_do_fork实现的。
4.2 创建进程在内核中的实现(_do_fork)

函数_do_fork的执行流程:1、调用copy_process创建新的进程2、如果参数clone_flags设置了标志C lONE_PARENT_SETTID,则把新线程的进程标识符写到参数parent_tidptr指定的位置3、调用wake_up_new_task唤醒新的进程4、如果系统调用vfork,则父进程挂亲等待子进程装载程序,直到子进程结束5、返回子进程的pid
4.3 Copy_process函数
在_do_fork函数中,创建进程的主要工作主要是由copy_process实现的,其参数和_do_fork函数保持一致,主要功能为创建进程描述符,子进程执行所需要的数据结构以及子进程的pid。Copy_process的主要步骤:
- 检查标志位
有以下几种情况:当出现如新的进程属于新的挂载/用户命名空间,与当前进程共享文件根目录;新进程与当前进程共享属于同一线程组,但是不共享信号处理程序或者不共享虚拟内存;新进程相与当前进程成为兄弟进城,但是当前进程是init进程;新进程与当前进程属于同一个线程组,但是新进程属于不同的进程号命名空间。 - 调用dup_task_struct
为当前进程获取新的进程描述符并为进程描述符分配内存。 -
检查进程的数量
如果当前拥有的进程数量超过限制则不允许创建进的进程。
检查进程的数量 - 调用copy_creds
调用copy_creds为新的线程复制或者共享证书。在eswin/kernel/cred.c中可以看到copy_creds函数,如果clone_flags设置了CLONE_THREAD,则新进程和当前进程共享证书,如果标志位为CLONE_NEWUSER,则为新进程创建新的用户命名空间。 -
检查线程数量
检查线程数量,如果超过允许的最大线程数量,则不允许创建。这个max_threads变量的值往往取决于系统内存容量的大小,总的原则是:所有thread_info和内核栈所占用的空间不能超过物理内存的1/8。
检查线程的数量 -
调用sched_fork
调用sched_fork函数为新进程设置调度相关的参数。该函数位于eswin/kernel/sched/core.c,函数首先给新进程的状态设置了TASK_NEW,但是没有哪个信号或者外部事件可以真的执行或者唤醒它,接着将新进程的调度优先级设置为当前进程的正常优先级以防止当前进程因为占有互斥锁而被提升了优先级。如果进程请求使用fork的默认值,则将优先级和调度策略设置为默认值。
sched_fork -
创建新的数据结构
调用copy_semundo,copy_files,copy_fs,copy_sighand,copy_signal,copy_mm,copyspaces,copy_io,copy_thread_tls来创建新的数据结构,并把父进程的数据结构的值复制到新数据结构中。
创建新的数据结构
- Copy_semundo()
Copy_semundo():位于eswin/ipc/sem.c中,属于用一个线程组的线程之间共享unix系统5信号量。 - Copy_files()
Copy_files():属于同一个线程组的线程之间共享打开文件表,如果clone_flags传入了CLONE_FLAGS,则表示共享打开文件表,新进程和当前进程共享文件表的files_struct,否则新进程直接把当前进程的打开文件表复制一份。 - Copy_fs()
Copy_fs():属于同一线程组的线程之间共享文件系统信息,包括根目录,当前工作目录等。如果clone_flags传入了CLONE_FS,则表示共享信息系统,新进程和当前进程共享文件系统信息的fs_struct,否则新进程直接把当前进程的文件系统信息复制一份。 - Copy_sighand()
Copy_sighand():同属于一个线程组的线程之间共享信号处理程序。如果clone_flags传入了CLONE_SIGHAND,则表示共享打开文件表,新进程和当前进程共享信号处理程序的结构体的sighand_struct,否则新进程直接把当前进程的信号处理程序复制一份。 - Copy_signal()
Copy_signal():同属于一个线程组的线程之间会共享信号结构体。如果clone_flags传入了CLONE_THREAD,则表示共享打开文件表,新进程和当前进程共享信号结构的sighand_struct,否则为新进程分配信号结构体并初始化继承当前进程的资源限制。 - Copy_mm()
Copy_mm():同一个线程组的线程之间共享虚拟内存。如果clone_flags传入了CLONE_VM,则表示共享虚拟内存,新进程和当前进程共享内存描述符mm_struct,否则新进程复制当前进程的虚拟内存。 -
Copy_namespce()
Copy_namespce():创建或者共享命名空间。源代码位于eswin/kernel/nsproxy.c。第144行如果任何标志位都没有设定,则新进程和当前进程共享命名空间nsproxy,第165行,根据flags的参数创建或者共享命名空间。
Copy_namespce() - Copy_io()
Copy_io():如果clone_flags传入了CLONE_IO, 表示共享I/O上下文,新的进程与父进程共享上下问的结构体io_context,否则创建新的I/O上下文,初始化并继承当前进程的I/O。 - Copy_thread_tls()
Copy_thread_tls():复制寄存器的值。调用copy_thread_tls()来复制当前进程的寄存机的值。进程有两处地方用来存放寄存器的值:当从用户模式切换到内核模式时,把用户模式的各种寄存器保存在结构体pt_regs中;当进程调度器调度进程时,切换出去的进程把寄存器的值保存在进程描述符的成员thread中。由于不同架构下的寄存器是不相同的,所以不同的架构需要定义自己的结构体pt_regs,来实现copy_thread_tls()。基于riscv的pt_regs存放在eswin/arh/riscv/include/asm/ptrace.h中,其中分别定了riscv所用到的寄存器。在arch目录下可以找到不同架构对应的不同结构体对应的寄存器。基于riscv架构copy_thread_tls()在eswin/arh/riscv/kernel/process.c目录下。
-
设置进程号和进程关系
Copy_progress的最后部分为新进程设置pid号和进程关系,1914-1919行为新进程分配进程号。
设置进程号
1951-1956为创建线程,当clone_flags传入的标志位CLONE_THREAD,把exit_signal设置为-1,新线程退出时不需要发送信号给父进程,因为新线程和当前线程属于一个线程组,成员group_leader指向与当前进程相同的组长,新进程tgid存放线程组的进程号和当前进程相同。1957-1962为创建进程,当clone_flags传入的标志位CLONE_PARENT,将exit_signal设置为当前进程所属线程组组长的成员exit_signal,如果没有传入标志位CLONE_PARENT,新进程的exit_signal的调用者指定的信号。新进程所属的线程组的组长是它自己。
创建线程
2001-2007为当前进程设定父进程,如果clone_flags有CLONE_PARENT或者CLONE_THREAD,那么子进程的父进程与当前进程的父进程相同,并设定id号。如果没有这两个参数,那么当前进程就是新进程的父进程。
设定父进程
最后部分还包括:判断新进程是否为init1号进程,因为1号进程是不能被杀死的;将新进程添加到进程,进程组和会话的进程链表中等
4.4 写时复制copy on write
子进程需要从父进程复制可写页面,本应该分配一个空闲的内存页面,再从父进程的页面把内容复制进来,然后建立映射,但是这个操作的代价有点大,因为复制过来的页面子进程不一定会使用,如果子进程只是进行了读访问而父子进程都没有进行写访问,完全是可以通过复制指针来共享这个页面的。所以linux内核采用了一种写时复制技术,首先是通过复制页面表项暂时共享这个页面,然后将父进程的页面表项改成保护,接着把已经改成写保护的表项设置到子进程的页面表中。这样一来,相应的页面在两个进程中都变成了已读,当不管是父进程还是子进程尝试对该页面进行写操作时,都会引起一次页面异常。处理页面异常时,反应是另外分配一个物理页面,将内容真正复制到新的物理页面中,让父子进程各自拥有属于自己的物理页面,然后将两个页面表中相应的表项改写。所以在linux中,内核是可以迅速实现“复制”的,其主要依赖的就是写时复制。

4.5 进程的调度
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int child;
char *args[] = {"/bin/echo","Hello","World",NULL};
if(!(child = fork()))
{
printf("pid %d : %d is my father\n", getpid(), getppid());
printf( "%d\n", execve("/bin/echo",args, NULL) );
printf("pid %d : I am back, something is wrong !\n", getpid());
}
else
{
int myself = getpid();
printf("pid %d : %d is my son\n", myself, child);
wait4(child, NULL, 0, NULL);
printf("pid %d : done\n", myself);
}
return 0;
}









