c有关

fork()底层实现

fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,
但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置

写时复制

when fork(), have to copy the entire parent’s address space
Use 写时复制 (CoW) to defer large copies as long as possible, hoping to avoid them altogether
Instead of copying pages, create shared mappings of parent pages in child virtual address space
Shared pages are protected as read-only in child
reads happens as usual
writes generate a protection fault, trap to OS, copy page, change page mapping in child page table, restart write instruction

fork()

分叉中的大部分工作由copy_process()处理,
在do_fork()中定义。由以下人员执行的操作:
它通过调用kernel/fork.c
它检查父级的do_fork()字段(即alloc_pid())
如果不是零,则父进程正由另一个进程跟踪
它调用ptrace,它设置进程描述符和子级执行所需的任何其他内核数据结构。
其参数与current->ptrace加上子对象的PID相同。
它检查在copy_process()参数中传递的标志是否兼容
它通过调用do_fork()和clone_flags
它调用security_task_create()为新进程创建新的内核堆栈、security_task_alloc()和dup_task_struct()结构。
新值与当前任务的值相同
此时,子进程描述符和父进程描述符是相同的
它执行thread_info宏以获得新进程的task_struct结构,并将其地址存储在alloc_task_struct()局部变量中。
它执行task_struct宏以获得一个空闲内存区域来存储新进程的tsk结构和内核模式堆栈,并将其地址保存在alloc_thread_info局部变量中。
它将当前进程描述符的内容复制到thread_info指向的ti结构中,然后将task_struct设置为tsk
它将当前的tsk->thread_info描述符的内容复制到ti指向的结构中,然后将thread_info设置为ti
它将新进程描述符(即ti->task)的使用计数器设置为2,以指定进程描述符正在使用,并且相应进程处于活动状态(其状态不是tsk或tsk->usage)
它返回新进程的进程描述符指针(即EXIT_ZOMBIE)
EXIT_DEADthen checks if the maximum amount of processes for the current user has not been exceeded(e.greater than`max_threads)tskthen checks if the maximum amount of processes for the current user has not been exceeded(即
它通过清除或初始化copy_process()
它调用task_struct更新copy_flags()
清除了flags(表示任务是否使用超级用户权限)和task_struct标志。
设置了PF_SUPERPRIV标志(指示任务是否未调用’exec())。
它调用’init_sigpending(),清除挂起的信号
根据传递给PF_NOFREEZEcopy_process()的参数,然后复制或共享资源
打开文件
文件系统信息
信号处理程序
地址空间
它调用将父级和子级之间的剩余时间片拆分的PF_FORKNOEXEC
最后,它返回一个指向新子级的指针
然后,do_fork(),添加一个挂起的sched_fork()信号,以防设置了do_fork()标志或必须跟踪子进程(即在SIGSTOP中设置了CLONE_STOPPED标志)
如果未设置PT_PTRACED标志,它将调用执行以下操作的p->ptrace函数:
它调整父级和子级的调度参数
如果子级将在与父级和子级相同的CPU上运行,并且父级和子级不共享同一组页表(即清除了CLONE_STOPPED标志),则它将通过在父级之前将其插入父级的runqueue来强制子级在父级之前运行。如果子进程刷新其地址空间并在分叉后立即执行一个新程序,那么这个简单的步骤将产生更好的性能。如果让父级先运行,则写时复制机制将导致一系列不必要的页复制。
否则,如果子级将不在与父级相同的CPU上运行,或者父级和子级共享同一组页表(即
wake_up_new_task()flag set),它将子级插入父级runqueue的最后一个位置。
否则,如果设置了CLONE_VM标志,则会将子级置于CLONE_VM状态。
如果正在跟踪父进程,则它将子进程的PID存储在
CLONE_STOPPED的TASK_STOPPED字段并调用
ptrace_message,它基本上停止当前进程并向其父进程发送一个current信号。子级的“祖辈”是跟踪父级的调试器;ptrace_notify()信号通知调试器当前已分叉子级,可以通过查看SIGCHLD字段来检索其PID。
如果指定了SIGCHLD标志,它会将父进程插入等待队列,并挂起它,直到子进程释放其内存地址空间(即,直到子进程终止或执行新程序)
它通过返回子对象的PID来终止。

fork() 源码

http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.23.tar.bz2
linux-2.6.23/arch/x86/kernel/process.c里面是这样的fork() call的是sys_fork():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
asmlinkage long sys_fork(struct pt_regs *regs)
{
return do_fork(SIGCHLD, regs->rsp, regs, 0, NULL, NULL);
}

asmlinkage long
sys_clone(unsigned long clone_flags, unsigned long newsp,
void __user *parent_tid, void __user *child_tid, struct pt_regs *regs)
{
if (!newsp)
newsp = regs->rsp;
return do_fork(clone_flags, newsp, regs, 0, parent_tid, child_tid);
}

/*
* This is trivial, and on the face of it looks like it
* could equally well be done in user mode.
*
* Not so, for quite unobvious reasons - register pressure.
* In user mode vfork() cannot have a stack frame, and if
* done by calling the "clone()" system call directly, you
* do not have enough call-clobbered registers to hold all
* the information you need.
*/
asmlinkage long sys_vfork(struct pt_regs *regs)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->rsp, regs, 0,
NULL, NULL);
}

接下来是do_fork()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
struct pid *pid = alloc_pid();
long nr;

if (!pid)
return -EAGAIN;
nr = pid->nr;
if (unlikely(current->ptrace)) {
trace = fork_traceflag (clone_flags);
if (trace)
clone_flags |= CLONE_PTRACE;
}

p = copy_process(clone_flags, stack_start, regs, stack_size, \
parent_tidptr, child_tidptr, pid);


/*
* Do this prior waking up the new thread - the thread
* pointer might get invalid after that point,
* if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;

if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
}

if ((p->ptrace & PT_PTRACED) || \
(clone_flags & CLONE_STOPPED)) {
/*
* We'll start up with an immediate SIGSTOP.
*/
sigaddset(&p->pending.signal, SIGSTOP);
set_tsk_thread_flag(p, TIF_SIGPENDING);
}

if (!(clone_flags & CLONE_STOPPED))
wake_up_new_task(p, clone_flags);
else
p->state = TASK_STOPPED;

if (unlikely (trace)) {
current->ptrace_message = nr;
ptrace_notify ((trace << 8) | SIGTRAP);
}

if (clone_flags & CLONE_VFORK) {
freezer_do_not_count();
wait_for_completion(&vfork);
freezer_count();
if (unlikely (current->ptrace & \
PT_TRACE_VFORK_DONE)) {
current->ptrace_message = nr;
ptrace_notify \
((PTRACE_EVENT_VFORK_DONE << 8) | \
SIGTRAP);
}
}
} else {
free_pid(pid);
nr = PTR_ERR(p);
}
return nr;
}

pid_t的类型定义

源码引自 https://elixir.bootlin.com/linux/v2.6.23/source/include/asm-alpha/posix_types.h#L15
创建进程时经常会用到进程号的类型定义:pid_t。我们都知道这个类型定义实际上就是int型。但是在linux下的c中的头文件中这个定义到底是怎么定义的呢?今天就把以前找这个定义的过程贴出来:

1.在/include/linux/types.h中可以看到这样的定义

1
typedef __kernel_pid_t		pid_t;

可以看出pid_t 有被定义为kernel_pid_t类型的。

2.在文件/include/asm-alpha/posix_types.h中可以看到这样的定义

1
typedef int		__kernel_pid_t;

由此我们终于找到了pid_t的真实定义:实际他就是 int 类型的。

关于 execl():

1
2
3
4
5
6
int execl(const char *path, const char *arg, ...
/* (char *) NULL */);
int execlp(const char *file, const char *arg, ...
/* (char *) NULL */);
int execle(const char *path, const char *arg, ...
/*, (char *) NULL, char * const envp[] */);
**l指的是commanline里面的的东西打包成list pass进exec()**
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
**v指的是commanline里面的的东西打包成array of strings pass进exec(), 跟main()里面相同**
**p指的是pass进去的应该是PATH varable ,(ls -l , ps -a , code . 之类的环境变量)**
**e指的是可以pass进去enviorment,不过这太难了,先不讲,我尼玛连装系统都装不明白呢。**

例子:
pid_t pid = fork();
if(pid==0){execlp("code",".",NULL);}
else{execlp("ps","-a",NULL);}  
printf("这行并不会被执行");
return 0;

run之后会打开visual code然后命令行显示一堆英文字母,因为execl会直接return,所以最后printf不会被执行。

参考 https://www.coder.work/article/172915