linux里的进程与线程(上)

在学习linux的过程中,进程与线程可谓一对好兄弟,是必然要掌握的内容。

一:何所谓进程 何所谓线程

        进程:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配与调度的基本单位。

        线程:线程是操作系统进程调度器可以调度的最小执行单元。

(在第一次看到这样的描述时,我的感觉是完全摸不着头脑。既然线程是系统调度的最小执行单元,那么为什么还说进程是操作系统调度的基本单位?)

        事实上,在linux的设计早期,只有进程的概念,没有线程的概念。随着计算机技术的不断发展,操作系统需要顺应时代,充分发挥多核心cpu的计算能力,所以引入的线程的概念。同时为了尽量少的修改原有的架构,linux中的线程可以说是进程的一个阉割版,故而二者有着千丝万缕的联系。

        在《linux环境编程 从应用到内核》一书中,提到了将进程看作是线程组的说法,这样做十分方便我们理解线程与进程的关系。如果把进程看作一个线程组(每个进程中至少要有一个线程),系统为每一个线程组分配资源(例如内存),同一个线程组内的所有线程共享这些资源。系统又决定了不同的线程的执行顺序,(先决定当前所执行的进程,再决定进程内的哪一个或多个线程)故而操作系统调度的最小单元是线程,最小单位是进程。

         这里可以顺带提一下并行和并发。二者都是描述多任务时的执行方案的。不同的是,并发是交替进行,并行是同时执行。既然并行是交替执行的,为什么还要提一个‘并’字呢?我是这样理解的,计算机在同一时间要执行好多的任务,但是只有一个cpu,那么计算机就只好飞快的轮流执行各种任务,给人同时执行的错觉。故而体现出‘并’的意义。所以,提及多进程的,就是说并发。如果计算机有多个cpu,那么多线程就是在说并行。

二:PCB

        学机电的同学激动了,PCB不就是印刷电路板吗!不是这样的,操作系统里的PCB说的是进程控制块,是每一个进程都拥有的一个数据结构。linux中的实现是一种名为task_struct的结构体。

它记录了一下几个类型的信息:

1.状态信息,例如这个进程处于可执行状态,休眠,挂起等。

2.性质,由于unix有很多变种,进程有自己独特的性质。

3.资源,资源的链接比如内存,还有资源的限制和权限等。

4.组织,例如按照家族关系建立起来的树(父进程,子进程等)。

或者这样说:

1)进程状态:可以是new、ready、running、waiting或halted等。

(2)程序计数器:接着要运行的指令地址。

(3)CPU寄存器:如累加器、索引寄存器(Index Register)、堆栈指针以及一般用途寄存器、状况代码等,主要用途在于中断时暂时存储数据,以便稍后继续利用;其数量及类因计算机架构有所差异。

(4)CPU排班法:优先级、排班队列等指针以及其他参数。

(5)存储器管理:如分页表(Page Table)等。

(6)会计信息:如CPU与实际时间之使用数量、时限、帐号、工作或进程号码。

(7)输入输出状态:配置进程使用I/O设备,如磁带机

         如果要查一个进程的户口的话,拿到它的PCB,就可以知道关于它的全部信息。而所谓的资源分配,也是记录在PCB中的。PCB维护着一个进程对应的状态,虚拟内存地址,打开的文件,调度优先级,标识符等等关键信息。

三:TCP

          TCP是类似PCB的用以管理线程的数据结构。通俗且不负责任的说法,TCP是PCB组成部分。

四:结构造就的现实

           既然线程是进程的一部分,或者说进程是线程的线程组,同一组内的线程共享进程资源(例如全局变量,文件描述符,信号处理器,当前工作目录状态等),但是各自执行各自的函数,也就是有自己的堆栈,那么这样的组织形式会带来什么样的结果呢?

           线程最大的好处就是发挥了多核心CPU的优势,这一点无需多讲(实际上在深入了解linux编程之后,对于多线程编程还需要有特别多的注意点,比如伪共享就是一种性能杀手,发生伪共享的时候多线程并发挥不出多核心的优势。)。

            此外,由于线程是在同一个进程内的,共享进程的资源,也就是对于作用域是整个进程的变量或其他资源而言,不同线程是可以直接拿到并对其操作的。这一点也就使得线程拥有了一定意义上的速度优势。

            而进程因为资源不共享,各个进程之间想要进行通讯或者想要协力完成一些任务,就需要进行进程间通讯,常见的有管道PIPE,已经System V IPC下的消息队列,信号量,共享内存,POSIX IPC下的消息队列,信号量,共享内存等。高级的进程间通讯有基于UNIX域socket套接字技术的通讯。以及网络也是一种进程间通讯。

            无论是进程还是线程,对于资源的操作,都需要协调合作,这样就就需要各种各样的锁机制。线程我们用互斥量,读写锁实现对共享资源的保护,进程我们通常使用信号量设置合适的临界区来实现互斥或同步。互斥量,读写锁的生存空间,或者说作用域是在整个进程内的,而信号量是在内核中的。(这一点也与进程和线程的结构相互吻合。)

             另外,线程的引入使得我们需要考虑向一个进程发送信号,具体由哪一个线程负责接收与处理。

五:pid tgid 

        以往pid表示的就是进程id,但是现在进程已经变成线程组了。所以pid实际表示的是线程id,tgid表示的是线程组id,也是是进程id。主线程的id与进程的id是相同的。

        也就是所,调用getpid实际上得到的是tgid,而调用gettid得到的是pid。

         为了验证,执行这样的代码:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/syscall.h>
void* handler(void *a)
{
    int num = (int)a;
    int pid = getpid();
    int tid = syscall(SYS_gettid);
    printf("im pthread NO.%d,geipid:%d,gettid:%d\n",\
           num,pid,tid);
    pthread_exit(NULL);
}

int main()
{
    int i = 0;
    pthread_t p_t[5]={0};
    for(i = 0;i < 5; i++)
    {
        pthread_create(&p_t[i],NULL,handler,(void*)i);
    }
    for(i = 0;i < 5; i++)
    {
        pthread_join(p_t[i],NULL);
    }
    return 0;
}

得到的结果为:

im pthread NO.0,geipid:4302,gettid:4303
im pthread NO.1,geipid:4302,gettid:4304
im pthread NO.2,geipid:4302,gettid:4305
im pthread NO.3,geipid:4302,gettid:4306
im pthread NO.4,geipid:4302,gettid:4307

可以看到确实如此。

            但是,既然有线程id,为什么pthread_create函数还需要在第一个参数中记录线程id呢?直接返回一个pid不就可以了吗?

注意!这两个id是不同的。

注意!这两个id是不同的。

注意!这两个id是不同的。

           重要的事情说三遍。

修改上边的代码,把p_t里边的内容打印出来看一下。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/syscall.h>
void* handler(void *a)
{
    int num = (int)a;
    int pid = getpid();
    int tid = syscall(SYS_gettid);
    printf("im pthread NO.%d,geipid:%d,gettid:%d\n",\
           num,pid,tid);
    pthread_exit(NULL);
}

int main()
{
    int i = 0;
    pthread_t p_t[5]={0};
    for(i = 0;i < 5; i++)
    {
        pthread_create(&p_t[i],NULL,handler,(void*)i);
    }
    for(i = 0;i < 5; i++)
    {
        pthread_join(p_t[i],NULL);
    }

    for(i = 0;i < 5; i++)
    {
        printf("pthread_t:按整形打印%d,按地址打印%p\n",p_t[i],p_t[i]);
    }
    return 0;
}

    得到这样的结果:

im pthread NO.0,geipid:4660,gettid:4661
im pthread NO.4,geipid:4660,gettid:4665
im pthread NO.3,geipid:4660,gettid:4664
im pthread NO.2,geipid:4660,gettid:4663
im pthread NO.1,geipid:4660,gettid:4662
pthread_t:按整形打印1201022720,按地址打印0x7f9347962700
pthread_t:按整形打印1192630016,按地址打印0x7f9347161700
pthread_t:按整形打印1184237312,按地址打印0x7f9346960700
pthread_t:按整形打印1175844608,按地址打印0x7f934615f700
pthread_t:按整形打印1167451904,按地址打印0x7f934595e700

   首先可以看出的是,两次执行的tgid是不同的,但同一次执行内的tgid是相同的,再次印证了上面的内容。而p_t中记录的线程id,与系统调用gettid得到的不是同一个值,看起来更像是某个地址。我用的是centos系统,在其他系统下,比如ios中pthread_t存储的数据可能又是另外的值。这是怎么回事?一个线程怎么会有两种id,并且不同系统下的id还会不一样?

六:两种线程id 

        gettid得到的线程id:《linux系统编程 从应用到内核》给出的解释是:线程是一种轻量级进程,是操作系统调度器的最小单位,所以需要一个线程id来操作线程。    也就是说,这的线程id是类似进程id的一种存在,对于每一个线程,内核中有且仅有一个数字将其标记,每个线程的id不同,通过线程id可以唯一的确定一个线程。

         pthread_t  pthread_self  pthread_equal 中的线程id

         这一线程id是用来标记一个进程内的线程的,通过上文我们知道,同一进程内的线程存在于进程的栈上,每个线程都有自己的栈地址。如果拿到了某个线程在栈上的起始位置,那么就可以标记这个线程。所以有些系统中线程的地址填充线程id。因为进程中的地址都是虚拟地址,由mmap完成映射,所以两个不同进程内的线程可能有相同的虚拟地址,故而有时会有不同进程内的不同线程,拥有相同的线程id。

          不同操作系统对这一种线程id的实现是不同的,所以要注意以下几点:

           1)getpid gettid的返回都可以用int接受  也可以用pid_t 接受。但是pthread_t  pthread_self  pthread_equal中的线程id只能用pthread_t接受

            2)因为不知道pthread_t的具体实现,固一般不打印pthread_t

            3)同上,因为不知道ptread_t的具体实现,为了保证抑制性和正确性,最好使用pthread_equal来判断两个线程id是否相同。

猜你喜欢

转载自blog.csdn.net/qq_15689151/article/details/82972602