6

c语言实战29,linux 内核是如何创建进程,线程的?

 3 years ago
source link: https://blog.popkx.com/c-language-actual-warfare-29-how-does-the-linux-kernel-create-processes-and-threads/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我们在 linux 学习系列文章的第 8 节中提到,对于 linux 来说,线程只是一种和其他一些进程共享某些资源的特殊进程而已(例如地址空间),linux 并不严格区分线程和进程。这种设计是简洁的,因为只要使用一套 C 语言函数,和适当的参数设置,就可以实现进程和线程的创建。

关于 linux 系统的设计和原理,可以点我查看 《linux 学习系列》文章。

9f1b3cd6b34df55456cff070258e6282.png

linux 内核如何创建进程和线程

linux 创建线程和进程的过程非常类似,都是主要依赖 clone 函数,只不过传入的参数不同而已。如此一来,内核只需要实现一个 C 语言函数,就既能创建进程,又能创建线程了,这是相当高雅的做法。

例如,创建进程的大致参数为:

clone(SIGCHLD)

创建线程的大致参数为:

clone(CLONE_VM | CLONE_FS  | CLONE_FILES | SIGCHLD)

传递给 clone 的参数决定了 clone 的具体行为。与创建进程相比,创建线程多出的 CLONE_VM | CLONE_FS | CLONE_FILES 参数指定了父子进程共享地址空间,文件系统资源,文件描述符等信息。下面介绍一下 clone 函数。

C 语言函数 clone

在 linux 中输入 man clone,即可得到它的说明书:

466d2aed729b41f5a58a852fc576c639.png

使用 clone 函数,需要包含 <sched.h> 头文件,它的 C 语言原型如下:
int clone(int (*fn)(void *), void *child_stack,
                 int flags, void *arg, ...
                 /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

能够看出,clone 函数是一个不定参数的函数。它主要用于创建新的进程(也包括线程,因为线程是“特殊”的进程),调用成功后,返回子进程的 tid,如果失败,则返回 -1,并将错误码设置再 errno。

clone 函数的第一个参数是一个函数指针,关于函数指针,可参考《》。第二个参数是用于创建子进程的栈,要注意的是,很多情况下,栈都是向低地址方向增长的,所以传入参数时需要将栈的高地址传入。它的第三个参数,就是用于指定行为的参数了。

按照上面的描述,我们更关心的是 clone 函数的 flags 参数,常用的几个参数的意义列举如下:

  • CLONE_FILES,父子进程共享打开的文件
  • CLONE_FS,父子进程共享文件系统信息
  • CLONE_VM,父子进程共享地址空间
  • CLONE_SIGHAND,父子进程共享信号处理函数,以及阻断的信号
  • CLONE_THREAD,父子进程放入相同的线程组

其他的参数,请参照 man 手册。介绍完 clone 函数,我们来看两个实例。

805a6aa07518430f61cb4ad6957a4e44.png

C 语言实例

我们以使用 clone 函数创建线程为例。以下函数每隔 1 秒创建一个线程,线程打印出自己的 tid 就退出。

#define _GNU_SOURCE
#include "stdio.h"
#include "stdlib.h"
#include "sched.h"
#include <unistd.h>
#include <sys/syscall.h>
#define     gettid()                    ((int)syscall(SYS_gettid))
void* testOcupMem(void* p)
{
    printf("    %d-%d testOcupMem exit\n", getpid(), gettid());
    return NULL;
}
#define STACK_SIZE 8192*1024

int main()
{
    int i = 5;

    void* pstk = malloc(STACK_SIZE);
    if(NULL == pstk){
         printf("query failed, cannot malloc stack\n");
         return -1;
    }
    while(i--){
        int clonePid = clone((int(*)(void*))testOcupMem,  (char *)pstk + STACK_SIZE,
                     CLONE_VM | CLONE_FS  | CLONE_FILES | SIGCHLD, pstk);
        printf("  clonePid: %d\n", clonePid);
        if(-1 ==  clonePid){
            printf("query failed, cannot start query process\n");
            free(pstk);
            return -1;
        }
        sleep(1);
    }
    free(pstk);
    printf("\n------------- getchar --------------\n");
    getchar();
    return 0;
}

头条函数排版太乱,代码可参照下图:

bc15cc04fda9b613df5ada078c80ee52.png

编译并执行 C 语言代码,发现线程的确被创建了:
# gcc t2.c
# ./a.out 
  clonePid: 25299
    25299 testOcupMem exit
  clonePid: 25300
    25300 testOcupMem exit
  clonePid: 25301
    25301 testOcupMem exit
  clonePid: 25302
    25302 testOcupMem exit
  clonePid: 25303
    25303 testOcupMem exit

------------- getchar --------------
#

这样我们就使用 C 语言的 clone 函数创建了线程。创建进程的过程是类似的,唯一需要做的就是改变 clone 函数的 flags 参数,即将

clonePid = clone((int(*)(void*))testOcupMem,  (char *)pstk + STACK_SIZE,
                     CLONE_VM | CLONE_FS  | CLONE_FILES | SIGCHLD, pstk);
clonePid = clone((int(*)(void*))testOcupMem,  (char *)pstk + STACK_SIZE,
                    SIGCHLD, pstk);

即可。

b5fb855ad1fa26dfc2108ee78ea8ca86.png

容易看出,linux 内核没有严格区分线程和进程,也没有准备特别的调度算法或是定义特别的数据结构来描述线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程,在 linux 中,线程看起来就像是一个普通的进程。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK