Linux多线程(Clone函数的深入研究)

线程是一种允许程序一次执行多个操作的机制。 与进程一样,线程似乎同时运行。

POSIX

Unix(POSIX)的便携式操作系统接口是IEEE计算机协会规定的一系列标准,用于维护操作系统之间的兼容性。
POSIX定义了应用程序编程接口(API),以及命令行shell和实用程序接口,以便与Unix和其他操作系统(Windows)的变体兼容。

  • 是IEEE API标准,包括线程API的标准。
  • 实现POSIX Threads标准的库通常被命名为Pthreads。
  • Pthreads最常用于类似UNIX的POSIX系统,如Linux和Solaris,但也存在Microsoft Windows实现。

API的实现可以在许多类似于Unix的POSIX一致的操作系统上使用,例如freeBSD,NetBSD,OpenBSD,Linux,MAX OS X,Android和Solaris,通常捆绑为库libpthread

GNU / Linux实现了POSIX标准线程API(称为pthreads)。 所有线程函数和数据类型都在头文件中声明。 pthread函数不包含在标准C库中。 相反,它们处于libpthread中,因此在链接程序时应将-lphtread添加到命令行。

创建一个线程(Thread Creation)

//pthread_create - 用于创造一个新的线程
#include
int pthread_create(pthread_ *thread, pthread_attr_t *attr, void*(*start_rountine)(void *arg), void *arg);
  • pthread_t是用于唯一标识线程的数据类型。 它由pthread_create()返回,并由应用程序在需要线程标识符的函数调用中使用。
  • 创建运行start_routine的线程,arg作为唯一参数。 如果pthread_create()成功完成,则线程将包含已创建线程的ID。 如果失败,则不会创建新线程,并且线程未定义。
  • attr定义了线程的属性,主要用于实时编程。
  • 第一个参数为指向线程标识符的指针。
  • 第二个参数用来设置线程属性。
  • 第三个参数是线程运行函数的起始地址。
  • 最后一个参数是运行函数的参数。


///threadcreate.c first part
#include
#include
#include

#include
#include
#define gettidv1() syscall(__NR_gettid)
#define gettidv2() syscall(SYS_gettid)
#define getpid() syscall(SYS_getpid)
/*系统调用 - 间接系统调用
syscall()是一个小型库函数,它调用系统调用,其汇编语言接口具有指定参数的指定编号。*/

void *thread_function(void *ptr)
{
    char *message;
    
    message = (char *)ptr;
    printf("in thread tid=%ld,pid=%ld\n",gettidv2(),getpid(),message);
    sprintf(ptr,"hello from child tid=%ld pid=%ld\n",gettidv1(),getpid());
}

int main(void)
{
    pthread_t thread1,thread2;
    char *message1 = "Thread 1";
    char *message2 = "Thread 2";
    int iret1, iret2;
    
    char buf[100];
    sprintf(buf,"hello from parent pid=%ld\n",getpid());
    printf("\nin main process pid=%ld\n original buf=%s\n",getpid(),buf);
    
    //创建独立的线程,每个线程都将执行函数
    iret1 = pthread_create(&thread1,NULL,thread_function,(void*)buf);
    iret2 = pthread_create(&thread2,NULL,thread_function,(void*)buf);
    
    /*在主函数继续之前等待线程完成。除非我们等待,
    否则我们会冒执行退出的风险,这将终止进程以及线程完成之前的所有线程。*/
    pthread_join(thread1,NULL);
    pthread_join(thread2,NULL);
    
    printf("in parent process %ld\n inputs buf=%s\n", getpid(),buf);
    exit(0);
}

线程以等待的方式结束(Join Threads)

一种解决方案是强制主函数等待其他两个线程完成。 我们需要的是一个类似于wait的函数,用于线程完成一个进程的内部。
该函数是pthread_join,它接受两个参数:*要等待的线程的线程ID,以及指向将接收完成线程的返回值的void 变量的指针。 如果您不关心线程返回值,则将NULL作为第二个参数传递。

pthread_join - 使用已终止的线程加入

#include
int pthread_join(pthread_t thread, void ** retval);

pthread_join()函数等待线程指定的线程终止。当函数返回时,被等待线程的资源被收回。如果该线程已经终止,则pthread_join()立即返回。 线程指定的线程必须是可连接的。
在成功时,pthread_join()返回0; 出错时,它会
返回错误编号

给线程传递数据

  • thread参数提供了一种将数据传递给线程的便捷方法。
  • 使用thread参数,很容易为许多线程重用相同的线程函数。 所有这些线程执行相同的代码,但是在不同的数据上。


Makefile

CLAGS = -g

tc:threadcreate.o
    gcc $(CLAGS) -o tc threadcreate.o -lpthread
    
threadcreate.o: threadcreate.c
    gcc $(CFLAGS) -c threadcreate.c


  • 所有线程都具有相同的进程ID。
  • 一个线程中更改的缓冲区可以被另一个线程看到(它们共享相同的内存空间)

GNU/Linux 线程实现

GNU / Linux上POSIX线程的实现与许多其他类UNIX系统上的线程实现不同:

  • 在GNU / Linux上,线程是作为进程实现的。 每当您调用pthread_create创建新线程时,Linux都会创建一个运行该线程的新进程。
  • 但是,此过程与使用fork创建的过程不同; 特别是,它与原始进程共享相同的地址空间和资源。

进程 VS 线程

  • 在Linux中,线程只是共享一些资源的任务,最明显的是它们的内存空间;
  • 另一方面,进程是不共享资源的任务。
  • 对于应用程序编程人员,以非常不同的方式创建和管理进程和线程。
    · 对于进程,有fork,wait等进程管理API。
    · 对于线程,有pthread库。
  • 但是,这些API和库,进程和线程都是通过单个Linux系统调用克隆来实现的。

clone系统调用

虽然在同一程序中创建的GNU / Linux线程是作为单独的进程实现的,但它们共享其虚拟内存空间和其他资源

  • 但是,使用fork创建的子进程可以获取这些项的副本。
  • Linux clone系统调用是fork和pthread_create的通用形式,它允许调用者指定在调用进程和新创建的进程之间共享哪些资源。
  • clone()的主要用途是实现线程:在共享内存空间中并发运行的程序中的多个控制线程。
  • 与fork()不同,这些调用允许子进程与调用进程共享其执行上下文的一部分,例如内存空间,文件描述符表和信号处理程序表。

clone-创建一个子进程

#include
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg,...);
  • 使用clone()创建子进程时,它将执行函数fn(arg)。 fn参数是指向子进程在执行开始时调用的函数的指针。 arg参数传递给fn函数。
  • child_stack参数指定子进程使用的堆栈的位置。
  • 虽然属于同一进程组的克隆进程可以共享相同的内存空间,但它们不能共享相同的用户堆栈。 因此,clone()调用为每个进程创建单独的堆栈空间


我们可以将clone视为进程和线程之间共享的统一实现。(这里下面的是重点)

  • Linux上进程和线程之间的区别是通过将不同的标志传递给克隆来实现的。
  • 差异主要在于这个新流程与启动它的流程之间共享的内容
  • 以下示例代码演示了线程最重要的共享方面 - 内存。 它以两种方式使用clone,一次使用CLONE_VM标志,一次不使用。

CLONE_VM(since Linux 2.0)

  • 如果设置了CLONE_VM,则调用进程和子进程在同一内存空间中运行。 特别是,由调用进程或子进程执行的内存写入在另一个进程中也是可见的。
  • 如果未设置CLONE_VM,则子进程在clone()时在调用进程的内存空间的单独副本中运行。 其中一个进程执行的内存写入不会影响另一个进程,就像fork一样。
  • 在没有vm命令行参数的情况下调用时,CLONE_VM标志关闭,父节点的虚拟内存被复制到子节点中。 子节点看到父节点放在buf中的消息,但无论写入buf的是什么,都会进入自己的副本而父节点无法看到它。
  • 但是当传递vm参数时,会设置CLONE_VM并且子任务共享父级的内存。 现在可以从父母那里看到它写入buf的内容。
///clone_vm.c part 1
#define _GNU_SOURCE
#include
#include
#include

#include
#include
#include

#include
#include
#define gettidv1() syscall(__NR_gettid)//syscall(__NR_gettid)函数的意思就是获取线程ID,这句话的意思是宏定义gettidv1()函数为获取线程id的函数
#define gettidv2() syscall(SYS_gettid)//这两个syscall没有任何区别,所以函数两个gettidv也没有任何的区别
#define getpid() syscall(SYS_getpid)//通过宏定义的方式获取pid

#define STACK_SIZE 1024*1024

static int child_func(void *arg)
{
    char* buf = (char*)arg;
    printf("in child sees buf = %s \n",buf);
    sprintf(buf,"hello from child tid=%ld,pid=%ld\n",gettidv1(),getpid());
    
    return 0;
}

命令行参数

有一种方法可以在程序开始执行时将命令行参数或参数传递给它。 调用main函数时,会使用两个参数调用它。

  • 第一个(通常称为argc,用于参数计数)是调用程序的命令行参数的数量;
  • 第二个(argv,for argument vector)是一个指向包含参数的字符串数组的指针,每个字符串一个。
  • 如果argc为1,则程序名称(argv [0])后面没有命令行参数。
///clone_vm.c part 2
int main(int argc,char **argv)
{
    //给子任务分配参数
    char *stack = malloc(STACK_SIZE);
    if(!stack){
        perror("malloc");
        exit(1);
    }
    
    //看看第一个命令行参数
    printf("\nthe programme name is %s\n",argv[0]);
    
    //唤起命令行参数vm,设置CLONE_VM标记
    unsigned long flags = 0;
    if((argc>1)&&(!strcmp(argv[1],"vm")))
        flag |= CLONE_VM;
        
    char buf[100];
    sprintf(buf,"hello from parent pid=%ld\n",getpid());
    printf("in parent process before clone\n buf=%s",buf);
    
    //根据不同的标记创建子进程
    if(clone(child_func,stack+STACK_SIZE,flags|SIGCHLD,buf)==-1)
    {
        perror("clone");
        exit(1);
    }
    
    int status;
    if(waitpid(-1,&status,0)==-1)
    {
        perror("wait");
        exit(1);
    }
    
    printf("in parent process: child exited with status %d.\n buf = %s \n",status,buf);
    return 0;
}

Makefile

clone_vm.o:clone_vm.c
    gcc $(CFLAGS) -c clone_vm.c
    
clone_vm:clone_vm.o
    gcc $(CFLAGS) -o clone_vm clone_vm.o
    
clean:
    rm -f *.o tc clone_vm



你可能感兴趣的:(操作系统,Linux)