clone&vfork&fork系统调用处理

clone日志

type=SYSCALL msg=audit(1714980638.367:4663): arch=c000003e syscall=56 success=yes exit=2580 a0=1200011 a1=0 a2=0 a3=7fdca0ac7bd0 items=0 ppid=1 pid=924 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="sshd" exe="/usr/sbin/sshd" subj=unconfined key="my_syscall_watch"ARCH=x86_64 SYSCALL=clone AUID="unset" UID="root" GID="root" EUID="root" SUID="root" FSUID="root" EGID="root" SGID="root" FSGID="root"
type=PROCTITLE msg=audit(1714980638.367:4663): proctitle=2F7573722F7362696E2F73736864002D44

分析处理

 1. 实际处理中,a0是flags的数据。如果符合sigchild,Vm,Vfork则表示vfork系统调用:0x4900  否则,如果符合sigchild==0x800则是fork系统调用

 2.如果是fork或vfrok按照其系统调用的方法处理,否则按照clone的方法处理

3.如果VM—0x100共享内存、FILES共享文件描述符0x400、FS 共享文件系统Ox200  根据这些信息更新进程的状态。

4. 如果是同一线程组0x10000,则处理线程相关的状态管理

5.最后创建两个进程之间的WasTriggeredBy边

通过  flags&flag==flag来判断是否存在此flag

在这个特定的日志中,flags 的值为 1200011。这是一个十六进制数。让我们将其转换为二进制,以便更好地理解:

十六进制: 1200011

解释:

  • CLONE_CHILD_CLEARTID (0x00200000):
    • 这个标志指示内核在子线程退出时将指定的内存位置清零。该内存位置由 child_tid 参数指定,通常用于线程库来处理线程退出的同步。
  • CLONE_CHILD_SETTID (0x01000000):
    • 这个标志指示内核在子线程启动时将其线程ID写入到指定的内存位置。该内存位置由 child_tid 参数指定。

 

 

系统调用

在 Linux 操作系统中,clone 系统调用的原型定义在内核头文件中。具体来说,它通常定义在内核源代码的 sched.h 文件中,这个文件包含了与进程调度和创建相关的函数原型和结构体定义。

https://man7.org/linux/man-pages/man2/clone.2.html

参考https://www.cnblogs.com/charlieroro/p/14280738.html

用户空间

在用户空间,系统调用通过标准库(如 glibc)来封装并提供给应用程序使用。这些原型定义通常位于标准库的头文件中,例如 unistd.hsyscall.h 等。

// 对于用户空间程序来说,要使用 clone 系统调用,需要头文件:  #include <sched.h>
在 sched.h 中,clone 系统调用的原型定义如下:
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ... /* pid_t *ptid, void *newtls, pid_t *ctid */ );

clone 系统调用用于创建一个新的进程或线程,并在新进程或线程中执行指定的函数。它与 fork 系统调用类似,但是具有更灵活的选项。

  • fn:是一个函数指针,用于指定新进程或线程要执行的函数。这个函数应该返回一个整数,并接受一个 void * 类型的参数,可以为空指针(NULL)。这个函数通常用来作为新进程或线程的入口点。

  • child_stack:是一个指向新进程或线程的栈空间的指针。在 Linux 中,栈空间通常是从高地址向低地址增长的,因此 child_stack 指向的是栈空间的顶端。如果 child_stack 为 NULL,则新进程或线程会共享调用者的栈空间。

  • flags:是一个位掩码,用于指定创建新进程或线程的行为和属性。可以使用的标志包括 CLONE_VM(共享地址空间)、CLONE_FS(共享文件系统信息)、CLONE_FILES(共享文件描述符表)等等。

  • arg:是传递给新进程或线程入口函数的参数。

  • ...:可选参数,用于传递一些额外的参数,例如新进程的 PID、新线程的 TLS(线程局部存储)等。

clone 系统调用成功时返回新进程或线程的 PID(对于新进程),或者 0(对于新线程);失败时返回 -1,并设置 errno 来指示错误的原因。

内核源码

 Linux 内核源代码,clone 系统调用的实现可以在 kernel/fork.c 文件中找到。在内核源代码中,clone 的原型可能定义为:

long sys_clone(unsigned long clone_flags, unsigned long newsp, void *parent_tid, void *child_tid, unsigned long tls);

 

重要标志

一些常用的 clone 标志位包括:  

clone 系统调用的标志可以在 Linux 的 sys/clone.h 头文件中找到

  • CLONE_VM:子进程与父进程共享同一个内存空间。
    •  如果设定了CLONE_VM ,则调用进程和子进程会运行在系统的内存空间中。调用进程或子进程对内存的写操作都可以被对方看到。此外使用mmap(2) 或munmap(2)执行的映射或去映射也会影响到另外一个进程。
  • CLONE_FS:子进程与父进程共享同一个文件系统信息(当前工作目录和根目录)。
    • 如果设置了CLONE_FS,则调用进程和子进程会共享相同的文件系统信息,包括文件系统的根,当前工作目录以及umask。任何一方(调用进程或子进程)执行了chroot(2)chdir(2), 或 umask(2),都会影响到另一方。
    • 如果没有设置CLONE_FS,则在执行clone系统调用时,子进程会继承调用进程的一份文件系统信息的拷贝。此时执行chroot(2)chdir(2), 或 umask(2)不会影响到另一方。
  • CLONE_FILES:子进程与父进程共享文件描述符表。
    • 如果设置了CLONE_FILES,则调用进程和子进程会共享相同的文件描述符表。调用进程或子进程创建的文件描述符同样对对方有效。类似地,如果某个进程关闭了文件描述符,或变更了相关的标志(使用fcntl(2) F_SETFD操作),同样会对其他进程生效。
    • 如果没有设置CLONE_FILES ,则在执行clone调用时,子进程会继承调用进程的所有打开的文件描述符,后续任何一方的打开、关闭文件描述符,或修改文件描述符标志等操作都不会影响到对方。注意,如果子进程中的文件描述符与调用进程中对应的文件描述符指向相同的(打开的)文件,则会共享相同的文件偏移和文件状态标志。
  • CLONE_SIGHAND:子进程与父进程共享信号处理程序。
  • CLONE_THREAD:子进程与父进程属于同一个线程组。
    • 如果设置了该标志,则子线程会放到与调用进程相同的线程组中。为了防止概念混淆,术语"线程"指代一个线程组中的进程。

      线程组是Linux 2.4中添加的一项功能,用于支持一组POSIX线程共享一个PID。在内部,该共享的PID是线程组的线程组标识符(TGID)。从Linux 2.4开始,getpid(2)会返回调用者的TGID。

      组中的线程可以通过其(系统范围内的)唯一线程ID(TID)进行区分。新线程的TID可用作返回给调用方的结果,线程可以使用gettid(2)获得自己的TID。

      当一个clone调用没有指定CLONE_THREAD时,生成的线程会放到一个新的线程组中,其TGID等于该线程的TID,该线程为新线程组的leader。

  • CLONE_PARENT:子进程的父进程设置为与调用进程的父进程相同。
  • CLONE_NEWCGROUP:在新的cgroup命名空间中创建进程。

    • 如果没有设置该标志,则新创建的进程与调用进程的cgroup命名空间相同。只有特权进程(CAP_SYS_ADMIN)才可以设置CLONE_NEWCGROUP

do_fork 函数是 forkvforkclone 等系统调用的核心实现函数。它根据传入的参数创建新进程,并设置新进程的各种属性。

 1 long do_fork(unsigned long clone_flags,
 2              unsigned long stack_start,
 3              struct pt_regs *regs,
 4              unsigned long stack_size,
 5              int __user *parent_tidptr,
 6              int __user *child_tidptr)
 7 {
 8     struct task_struct *p;
 9     int trace = 0;
10     long nr;
11 
12     if (unlikely(current->flags & PF_NPROC_EXCEEDED))
13         return -EAGAIN;
14 
15     p = copy_process(clone_flags, stack_start, regs, stack_size,
16                      child_tidptr, NULL, trace);
17     /*
18      * Do this prior waking up the new thread - the thread pointer
19      * might get invalid after that point, if the thread exits quickly.
20      */
21     if (!IS_ERR(p)) {
22         struct completion vfork;
23         struct pid *pid;
24 
25         trace_sched_process_fork(current, p);
26 
27         pid = get_task_pid(p, PIDTYPE_PID);
28         nr = pid_vnr(pid);
29 
30         if (clone_flags & CLONE_PARENT_SETTID)
31             put_user(nr, parent_tidptr);
32 
33         if (clone_flags & CLONE_VFORK) {
34             p->vfork_done = &vfork;
35             init_completion(&vfork);
36             get_task_struct(p);
37         }
38 
39         wake_up_new_task(p);
40 
41         /* forking complete and child started to run, tell ptracer */
42         if (unlikely(p->ptrace))
43             ptrace_fork(p);
44 
45         if (clone_flags & CLONE_VFORK) {
46             if (!wait_for_vfork_done(p, &vfork))
47                 ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
48         } else {
49             ptrace_event_pid(PTRACE_EVENT_FORK, pid);
50         }
51 
52         put_pid(pid);
53     } else {
54         nr = PTR_ERR(p);
55     }
56 
57     return nr;
58 }
View Code

参数:

  • clone_flags:用于指定进程的创建方式和属性(如 SIGCHLD)。
  • stack_start:新进程的用户堆栈指针。
  • regs:进程的寄存器上下文。
  • stack_size:堆栈大小。
  • parent_tidptrchild_tidptr:用户空间的 TID 指针。

do_fork 返回新进程的 PID。

copy_process 函数负责复制当前进程的任务结构(task_struct)并设置新进程的各种属性。

  • wake_up_new_task:唤醒新创建的进程,使其开始运行。
  • 处理 CLONE_VFORK 标志:如果设置了 CLONE_VFORK,父进程将等待子进程执行。

 

sys_fork 函数通过 do_fork 函数创建一个新的进程。sys_fork 函数通过 do_fork 函数创建一个新的进程。

asmlinkage long sys_fork(struct pt_regs *regs)
{
    return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
}

SYSCALL_DEFINE0(fork)
{
    return _do_fork(SIGCHLD, 0, 0, NULL, NULL);
}
View Code

 sys_vfork

/*
 * sys_vfork - create a child process and block the parent process
 * until the child exits or execs.
 */

SYSCALL_DEFINE0(vfork)
{
    return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 0, NULL, NULL);
}

 相关知识

  • Linux 内核中的每个进程都有一个与之对应的进程描述符。
  • Linux 内核中不同的进程间通信机制,如管道、消息队列、共享内存等
  • clone 调用可能会涉及到新进程的地址空间的创建和管理
  • clone 调用可能会涉及到新进程的调度

进程与线程

进程

  • 概念:进程是程序的执行实例。在计算机中,每个正在运行的程序都是一个进程,它拥有自己的内存空间、程序代码、数据和资源。

  • 特点:

    • 进程之间相互独立,一个进程的崩溃不会影响其他进程。
    • 进程有自己独立的地址空间,不能直接访问其他进程的内存。
    • 进程之间通过进程间通信(IPC)来交换数据和信息。
线程
  • 概念:线程是进程内的一个独立执行单元。一个进程可以包含多个线程,它们共享进程的地址空间和资源。

  • 特点:

    • 线程是轻量级的执行单元,相比进程,线程的创建、切换和销毁开销更小。
    • 同一进程内的线程共享进程的资源,包括内存空间、文件描述符等。
    • 线程之间可以直接访问共享内存,因此在共享数据时需要考虑线程同步和互斥

使用 clone 创建多线程或协程时需要了解的关键点:

clone线程操作
  1. 设置栈空间:需要为新线程分配栈空间。可以通过 mmap 等方式分配一块内存作为新线程的栈空间。

  2. 设置线程入口函数:需要指定新线程的入口函数,即线程开始执行的函数。这个函数需要符合线程函数的要求,通常是一个无返回值、无参数的函数。

  3. 设置参数:需要设置 clone 的参数,包括标志位和其他参数。其中,clone 的标志位可以用来控制新线程与原线程之间的资源共享程度,例如是否共享内存空间、文件描述符等。

  4. 调用 clone 系统调用:在主线程中调用 clone 系统调用,传递指定的参数,即可创建新线程。

  在 Linux 中,如果使用 clone 系统调用创建一个线程,新线程的 task_struct 结构体中的 pid 字段会被设置为与父进程相同的值,即为 123。而新线程的 TID(任务 ID)是由内核分配的,通常在 thread_info 结构体中的 flags 字段中包含了线程的 TID,但并不是简单地将整个 TID 存储在 flags 字段中,而是经过一些计算或位运算处理,以便节省内存空间。

线程的PID与TID

Linux——一文彻底了解进程id和线程id的关系(什么是pid、tgid、lwp、pthread_t)-CSDN博客

根据链接可知:

进程task_struct结构体(进程PCB)中的 pid, tgid 属性

getpid获得的是task_struct中的 tgid,即线程组编号

gettid获得的是task_struct中的 pid,即线程编号

也就是说内核层面没有所谓的Tid,  PID就是Tid

线程组的主PID, 就是这个线程组的 TGID

在Linux中线程其实是通过轻量级进程实现的,也就是LWP(light weight process),因此在Linux中每个线程都是一个进程,都拥有一个PID,换句话说,操作系统原理中的线程,对应的其实是Linux中的进程(即LWP)

 

资源共享和管理

  • 内存共享:使用 CLONE_VM 标志创建的进程共享相同的内存空间,这样父子进程可以直接访问彼此的变量和数据结构。
  • 文件描述符共享:使用 CLONE_FILES 标志创建的进程共享同一个文件描述符表,可以访问和操作父进程已经打开的文件。
  • 文件系统共享:使用 CLONE_FS 标志创建的进程共享当前工作目录和根目录。
  • 信号处理共享:使用 CLONE_SIGHAND 标志创建的进程共享相同的信号处理程序,便于线程间的信号处理同步。
  • CLONE_THREAD:与父任务在同一个线程组中(即表现为线程)

命名空间

Linux 的命名空间(Namespace)机制是 Linux 内核提供的一种机制,用于实现进程之间的隔离。它允许将一组系统资源封装在一个抽象的环境中,每个进程拥有自己的命名空间,使得不同进程之间的资源看起来是彼此独立的。这种隔离性可以用于容器化技术、进程隔离、安全沙盒等场景。

Linux 提供了多种类型的命名空间,包括:

  1. PID 命名空间:使得在不同的命名空间中,相同的 PID 可以指代不同的进程,因此进程在不同的 PID 命名空间中可以拥有相同的 PID,从而实现进程隔离。

  2. Mount 命名空间:使得在不同的命名空间中,文件系统挂载点可以彼此隔离,进程在不同的 Mount 命名空间中拥有独立的文件系统视图。

  3. UTS 命名空间:用于隔离主机名和域名等系统标识符。

  4. IPC 命名空间:用于隔离 System V IPC 和 POSIX 消息队列等进程间通信机制。

  5. 网络命名空间:用于隔离网络设备、网络栈、端口等网络资源,使得每个命名空间拥有独立的网络配置。

  6. 用户命名空间:用于隔离用户 ID、用户组 ID 等用户标识符。

  7. Cgroup 命名空间:用于隔离控制组(cgroup),实现资源限制和管理。

这些命名空间可以单独使用,也可以组合使用,以实现更灵活的隔离和管理。

clone 系统调用的关系在于,clone 系统调用提供了创建新进程时指定命名空间的功能。通过 cloneflags 参数,可以指定新创建的进程继承或创建新的命名空间。因此,clone 系统调用是实现命名空间隔离的重要手段之一。容器化技术如 Docker 就是利用 clone 系统调用和命名空间机制实现进程隔离和资源隔离的。

信号

在 Unix/Linux 系统中,信号是一种用于进程间通信的基本机制,用于通知进程发生了特定的事件或条件。信号可以来自多个来源,包括硬件、操作系统内核、其他进程以及进程自身

  1. 硬件异常:硬件故障或异常(如除零错误、内存访问错误等)可能会导致操作系统向受影响的进程发送一个相应的信号,通知进程发生了错误。

  2. 操作系统事件:操作系统内核会在发生特定事件或条件时向进程发送信号。例如,当子进程退出时,父进程会收到 SIGCHLD 信号;当用户按下 Ctrl+C 组合键时,终端会向前台进程组发送 SIGINT 信号。

  3. 其他进程发送的信号:一个进程可以通过系统调用(如 kill())向另一个进程发送信号。这通常用于进程间通信或控制其他进程的行为。

  4. 定时器信号:进程可以设置定时器,当定时器到期时,操作系统会向进程发送一个相应的定时器信号,例如 SIGALRM。

  5. 软件产生的信号:某些软件可以通过特定的配置或条件来产生信号,以通知进程发生了特定的事件。例如,一些调试工具可以向进程发送信号来触发断点。

 信号处理

在 Linux 中,当一个进程(或线程)收到信号时,内核会调用一个与该信号相关联的处理程序来处理这个信号。

通常情况下,每个进程都有一组默认的信号处理程序,用于处理不同的信号。例如,对于 SIGINT 信号(例如用户按下 Ctrl+C 组合键时发送的中断信号),默认的处理程序是终止进程。

但是,Linux 允许进程为特定的信号自定义信号处理程序。这样,当进程收到这个信号时,就会执行自定义的处理程序而不是默认的处理程序。在多线程程序中,所有线程共享同一组信号处理程序。这意味着,当进程中的任何一个线程接收到信号时,都会调用相同的信号处理程序。

因此,"共享信号处理程序" 指的是所有线程共享相同的信号处理程序。

信号处理程序(Signal Handler)

信号处理程序(Signal Handler)是一段用户定义的代码,用于处理进程接收到的特定信号。

信号处理程序通常使用 signal()sigaction() 等系统调用进行注册。一旦信号被注册了处理程序,当进程接收到该信号时,内核就会调用相应的信号处理程序来执行特定的操作。在执行完信号处理程序后,控制权会返回到进程的正常执行流程中。

需要注意的是,信号处理程序在执行时是异步的,它会中断进程当前的执行流程来处理信号。

 

标志位文档

https://github.com/torvalds/linux/blob/master/include/uapi/linux/sched.h

clone 系统调用的标志可以指定新进程与父进程共享哪些资源。常见的标志有:

  • CLONE_VM:子进程和父进程共享同一个内存空间。用于线程创建,因为线程间需要共享内存。
  • CLONE_FS:共享文件系统信息(当前工作目录和根目录)。
  • CLONE_FILES:共享文件描述符表。
  • CLONE_SIGHAND:共享信号处理。
  • CLONE_PARENT:子进程的父进程是调用进程的父进程。
  • CLONE_THREAD:子进程和父进程在同一个线程组中。
  • CLONE_SYSVSEM:共享 System V 信号量。
  • CLONE_SETTLS:设置线程本地存储(TLS)。
  • CLONE_PARENT_SETTIDCLONE_CHILD_SETTID:设置线程 ID。
  • CLONE_CHILD_CLEARTID:在子进程退出时清除线程 ID。
  • CLONE_NEWNS:新的挂载命名空间。
  • CLONE_NEWPID:新的 PID 命名空间。
  • CLONE_NEWUTS:新的 UTS 命名空间。
  • CLONE_NEWUSER:新的用户命名空间。
  • CLONE_NEWIPC:新的 IPC 命名空间。
  • CLONE_NEWNET:新的网络命名空间。

(v)fork,execve,clone区别

fork系统调用:创建子进程,子进程具有与其父进程相同的数据。但是,这两个进程都有单独的地址空间。(等于是分成了两个进程)

vfork系统调用:vfork() 也会创建一个与其父进程相同的子进程。但是,子进程会暂时挂起父进程,直到它终止。

      由 vfork() 系统调用创建的子进程继承其父进程的属性。其中包括文件描述符、当前工作目录、信号处置等。

      它创建新进程,而无需复制父进程的地址空间。这在面向性能的应用程序中非常有用。

exec():         替换调用进程,但是pid相同。父进程终止,新进程从入口点开始。(相当于重新来过)

clone系统调用:clone() 系统调用是 fork 调用的升级版本。它功能强大,因为它创建子进程,并对父进程和子进程之间共享的数据提供更精确的控制。

        此系统调用的调用方可以控制文件描述符表、信号处理程序表以及两个进程是否共享同一地址空间。

 

fork()、vfork()、exec() 和 clone() 之间的区别 |Linux 上的 Baeldung
posted @ 2024-05-06 22:54  zzl14  阅读(129)  评论(0)    收藏  举报