Linux的线程——轻进程

进程类似于人生:它们被产生,有或多或少有效的生命,可以产生一个或多个子进程,最终都要死亡。一个微小的差异是进程之间没有性别差异——每个进程都只有一个父亲。那么,操作系统有一个重要的概念——线程,在Linux上是怎么实现的呢?可以明确的告诉你,Linux并没有线程这个概念。呵呵,是不是Linux很落后呢,不是,恰恰相反,Linux提供了另一个概念——轻进程,其更具有扩展性,更伟大。

Linux是支持多线程的功能的,只不过是通过一个概念——轻量级进程来实现的。

从内核观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的实体。当一个进程创建时,它几乎与父进程相同。它接受父进程地址空间的一个(逻辑)拷贝,并从进程创建系统调用(fork)的下一条指令开始执行与父进程相同的代码。尽管父子进程可以共享含有程序代码(正文)的页,但是它们各自有独立的数据拷贝(包括堆和栈,写时再拷贝),因此子进程对一个内存单元的修改对父进程是不可见的。

如今的UNIX内核早已摆脱了这种简单的进程创建模式,大多数UNIX系统支持多线程应用程序:拥有很多相对独立执行流的用户程序共享应用程序的大部分数据结构。在这样的系统中,一个进程由其他几个用户线程组成,每个线程都代表一个执行流。

而Linux内核的早期版本没有提供多线程应用的支持。从内核观点看,多线程应用程序仅仅是一个普通进程。多线程应用程序多个执行流的创建、处理、调度整个都是在用户态进行的。用户是通过使用C语言中,POSIX 1C提供的标准线程库来实现用户级线程,其中包括线程的创建、删除、互斥和条件变量的同步操作以及调度和管理线程标准函数,无需内核的支持。

但是,这种多线程应用程序的实现方式不那么令人满意。例如,ULK-3上一个著名的例子,假设一个人机大战象棋程序使用两个线程:其中一个控制图形化棋盘,等待人类选手的移动并显示计算机的移动,而另一个思考棋的下一步移动。尽管第一个线程等待选手移动时,第二个线程应当继续运行,以利用选手的思考时间。但是,如果象棋程序仅是一个单独的进程,第一个线程就不能简单地发出等待用户行为的阻塞系统调用;否则,第二个线程也被阻塞。因此,第一个线程必须使用复杂的非阻塞技术来确保进程仍然是可运行的。

现在的Linux使用轻量级进程对多线程应用程序提供了更好的支持。两个轻量级进程基本上可以共享一些资源,诸如地址空间、打开文件等等。只要其中一个修改共享资源,另一个就立即查看这种修改。当然,当两个线程访问共享资源时就必须同步它们自己。

那么,实现多线程应用程序的一个简单方式就是把轻量级进程与每个应用程序线程关联起来。这样,线程之间就可以通过简单地共享同一内存地址空间、同一打开文件集等来访问相同的应用程序数据结构集;同时,每个线程都可以由内核独立调度,以便一个睡眠的同时另一个仍然可以运行。POSIX兼容的多线程应用程序是由支持“线程组”的内核来处理的。在Linux中,一个线程组基本上就是实现了多线程应用的一组轻量级进程,对于像getpid(),kill(),和_exit()这样的一些系统调用,它像一个组织,起整体的作用。

在Linux中,当使用系统调用clone()系统调用时,它创建的新进程与被调用者共享同一个用户地址空间,从原理上来讲,这个新创建的进程是调用者进程的一个线程。但是Linux并不承认,因为内核并没有专门定义线程使用的数据结构,所以它的线程和进程在结构上并没有任何区别。这也是为啥Linux强大的原因,因为Linux进程体系的结构已经够合理了,不需要额外的数据结构就可以实现线程的功能,其实现线程的功能也是可以扩展的,因为你可以根据需要选择共享的资源。呵呵,是不是很伟大啊?

1 clone()系统调用

在linux中,轻量级进程是由名为clone()的系统调用创建的:
asmlinkage int sys_clone(struct pt_regs regs)
{
unsigned long clone_flags;
unsigned long newsp;
int __user *parent_tidptr, *child_tidptr;

扫描二维码关注公众号,回复: 1024751 查看本文章
clone_flags = regs.ebx;
newsp = regs.ecx;
parent_tidptr = (int __user *)regs.edx;
child_tidptr = (int __user *)regs.edi;
if (!newsp)
    newsp = regs.esp;
return do_fork(clone_flags, newsp, &regs, 0, parent_tidptr, child_tidptr);

}

实际上,clone()是C语言库中定义的一个封装函数,它负责建立新轻量级进程的堆栈并且启动clone系统调用。clone()所创建的进程可以制造出线程的效果,因为它会按照你给定的参数决定该共享哪些信息。其使用以下参数:
fn:指定一个由新进程执行的函数。当这个函数返回时,子进程终止。函数返回一个整数,表示子进程的退出代码。
arg:指向传递给fn()函数的数据。
flags:低字节指定子进程结束时发送到父进程的信号代码,通常选择SIGCHLD信号。剩余3个字节给一clone标志组用于编码。标志组如下表所示:

Flag name

Description

CLONE_VM

Shares the memory descriptor and all Page Tables .

CLONE_FS

Shares the table that identifies the root directory and the current working directory, as well as the value of the bitmask used to mask the initial file permissions of a new file (the so-called file umask ).

CLONE_FILES

Shares the table that identifies the open files .

CLONE_SIGHAND

Shares the tables that identify the signal handlers and the blocked and pending signals . If this flag is true, the CLONE_VM flag must also be set.

CLONE_PTRACE

If traced, the parent wants the child to be traced too. Furthermore, the debugger may want to trace the child on its own; in this case, the kernel forces the flag to 1.

CLONE_VFORK

Set when the system call issued is a vfork( ) .

CLONE_PARENT

Sets the parent of the child (parent and real_parent fields in the process descriptor) to the parent of the calling process.

CLONE_THREAD

Inserts the child into the same thread group of the parent, and forces the child to share the signal descriptor of the parent. The child’s tgid and group_leader fields are set accordingly. If this flag is true, the CLONE_SIGHAND flag must also be set.

CLONE_NEWNS

Set if the clone needs its own namespace, that is, its own view of the mounted filesystems ; it is not possible to specify both CLONE_NEWNS and CLONE_FS .

CLONE_SYSVSEM

Shares the System V IPC undoable semaphore operations .

CLONE_SETTLS

Creates a new Thread Local Storage (TLS) segment for the lightweight process; the segment is described in the structure pointed to by the tls parameter.

CLONE_PARENT_SETTID

Writes the PID of the child into the User Mode variable of the parent pointed to by the ptid parameter.

CLONE_CHILD_CLEARTID

When set, the kernel sets up a mechanism to be triggered when the child process will exit or when it will start executing a new program. In these cases, the kernel will clear the User Mode variable pointed to by the ctid parameter and will awaken any process waiting for this event.

CLONE_DETACHED

A legacy flag ignored by the kernel.

CLONE_UNTRACED

Set by the kernel to override the value of the CLONE_PTRACE flag (used for disabling tracing of kernel threads ).

CLONE_CHILD_SETTID

Writes the PID of the child into the User Mode variable of the child pointed to by the ctid parameter.

CLONE_STOPPED

Forces the child to start in the TASK_STOPPED state.

child_stack:表示把用户态堆栈指针赋给子进程的esp寄存器。调用进程(父进程)应该总是为子进程分配新的堆栈。
tls:表示局部存储段TLS数据结构的地址,该结构是为新轻量级进程定义的。只有在CLONE_SETTLS标志被设置时才有意义。
ptid:表示父进程的用户态变量地址,该父进程具有与新轻量级进程相同的PID。只有在CLONE_PARENT_SETTID标志被设置时才有意义。
ctid:表示新轻量级进程的用户态变量地址,该进程具有这一类进程的PID。只有在CLONE_CHILD_SETTID被设置时才有意义。

不过,实现clone()系统调用的sys_clone()例程好像并没有fn和arg等参数,只是一堆寄存器。别着急,因为封装函数将fn指针存放在子进程堆栈的某个位置处,该位置就是该封装函数本身返回地址存放的位置。arg指针正好存放在子进程堆栈中的fn下面。当封装函数结束时,CPU从堆栈中取回返回地址,然后执行fn(arg)函数。

传统的fork()系统调用在linux中是用clone()实现的,其中clone()的flags参数指定为SIGCHLD信号以及所有清0的clone标志,而它的child_stack参数是父进程当前的堆栈指针。因此,父进程和子进程暂时共享一个用户态堆栈。但是,只要父子进程中有一个试图去改变栈,则立即各自得到用户态堆栈的一份拷贝。

vfork系统调用在Linux中也是用 clone( )实现的,其中clone( )的参数flags指定为SIGCHLD信号和CLONE_VM以及CLONE_VFORK标志,clone()的参数child_stack等于父进程当前的栈指针。

猜你喜欢

转载自blog.csdn.net/zym1348010959/article/details/80241493