精华内容
下载资源
问答
  • 设计题目一:Linux进程线程控制 ——以生产者消费者为例实现进程线程控制 ——by  一、设计题目要求 加深理解进程和程序、进程和线程之间的联系与区别; 深入理解进程及线程的重要数据结构及实现机制; 熟悉进程及...

    操作系统课程设计实验报告


    设计题目一:Linux进程线程控制

    ——以生产者消费者为例实现进程线程控制

    ——by 


    一、设计题目要求

    加深理解进程和程序、进程和线程之间的联系与区别;

    深入理解进程及线程的重要数据结构及实现机制;

    熟悉进程及线程的创建、执行、阻塞、唤醒、终止等控制方法;

    学会使用进程及线程开发应用程序。

    二、程序设计思路及流程图

    程序功能简介:

    生产者功能描述:在同一个进程地址空间内执行两个线程。生产者线程生产物品,然后将物品放置在一个空缓冲区中供消费者线程消费。当生产者线程生产物品时,如果没有空缓冲区可用,那么生产者线程必须等待消费者线程释放出一个空缓冲区。 

    消费者功能描述:消费者线程从缓冲区获得物品,然后释放缓冲区,当消费者线程消费物品时,如果没有满的缓冲区,那么消费者线程将被阻塞,直到新的物品被生产出来。

    程序设计思路:

    设计了两个主要函数:生产者函数、消费者函数

    设计了三个信号量:full信号量,判断缓冲区是否有值,初值为0;

                      empty信号量,判断缓冲区是否有空缓冲区,初值为缓冲区数;

                      mutex信号量作为互斥信号量,用于互斥的访问缓冲区。

    生产者函数通过执行P操作信号量empty减1,判断缓冲区是否有空。有空则互斥的访问缓冲区病放入数据,然后释放缓冲区,执行V操作,信号量full加1.

    消费者函数执行P操作,信号量full减1,判断是否有数据,有则互斥的访问缓冲区并取走数据,然后释放缓冲区,执行V操作,empty信号量加1。

    程序流程图:

    见附录A

    三、涉及的背景知识及所用函数简介

    1、fork 函数

    函数原型 :pid_t fork(void)

    头文件 :unistd.h

    作用 :创建一个子进程

    返回值 :成功后父进程返回子进程的PID号,子进程返回0;失败返回-1

    2、pthread_create 函数

    函数原型 :int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_rtn) (void*),void * arg)

    头文件 :pthread.h

    作用 :创建一个线程

    参数 :thread         待创建线程的id指针

               pthread_attr_t     创建线程时的线程属性

               *start_rtn(void * ) 返回值是void*类型的指针函数

               arg                函数start_rtn的参数

    返回值 :成功返回0;失败返回错误编号

    3、pthread_join 函数

    函数原型 :int pthread_join( pthread_t thread, void ** rval_ptr)

    头文件 :pthread.h

    作用 :1)调用者将挂起并等待指定线程终止

                     2)当新线程调用pthread_exit()退出或者return时,进程中的其他线       可通过pthread_join()获得进程的退出状态

    参数 :thread 线程的ID

    rval_ptr 线程的返回状态

    返回值 :成功返回0;失败返回错误编码

        4、wait 函数

    函数原型 :pid_t wait(int *status)

    头文件 :sys/wait.h

    作用 :调用者将挂起并等待子进程终止

    参数 :status 进程的返回状态

    返回值 :子进程的ID号

    5、sem_wait 函数

    函数原型 :int sem_wait(sem_t*sem)     

    头文件 :semaphore.h       

    作用 :从信号量的值减去一个“1”,但它永远会先等待该信号量为一个

    零值才开始做减法    

    返回值 :所有的函数成功返回0,错误的话信号量的值不改动,返 回-1,error设定来标识错误

    6、sem_post 函数

    函数原型 :int sem_post(sem_t*sem)

    头文件 :emaphore.h  

    作用 :给信号量的值加上一个“1”

    返回值 :所有的函数成功返回0,错误的话信号量的值不改动,返 回-1,error设定来标识错误

    7、sem_init 函数

    函数原型 :int sem_init(sem_t*sem, int pshared, unsigned int value);

             头文件 :emaphore.h 

    作用 :初始化一个定位在sem的匿名信号量

    返回值 :成功是返回0;错误时,返回—1,并把error设置为合适的值。

    8、sem_destroy 函数

    函数原型 :int sem_destroy(sem_t*sem)

    头文件 :emaphore.h  

    作用 :销毁有sem指向的匿名信号量

         

     返回值 :成功是返回0;错误时,返回—1,并把error设置为合适的值。

    9、signal 函数

    函数原型 :void (*signal(int signum,void(*handler)(int)))(int);           

    头文件 :signal.h

    作用 :设置某一信号的对应动作

    参数 :sinnum指明了所要处理的信号类型

                   handler描述了与信号关联的动作

    返回值 :返回先前的信号处理函数指针,如果有错误则返回 SIG_ERR(-1)

    10、alarm 函数

    函数原型 :unsigned int alarm (unsigned int seconds);

    头文件 :unistd.h

    作用 :在进程中设置一个定时器,当定时器指定的时间到时,它向进程 发送SIGALRM信号。如果忽略或者不捕获此信号,则其默认动作      是终止调用该alarm函数的进程。

    参数 :seconds  制定秒数

     返回值 :成功:如果调用此alarm()前,进程已经设置了闹钟时间,则返 回上一个闹钟时间的剩余时间,否则返回0。出错:-1

    11、getpid 函数

    函数原型 :int_getpid(void)

    头文件 :unistd.h

    作用 :取得进程识别代码

    返回值 :目前进程的进程识别码

    12、srand 函数

    函数原型 :void srand(unsigned seed);         

    头文件 :stdlib.h

    作用 :产生伪随机数序列

    参数       :seed   改变系统提供的种子植

    13、Pthread_setcancel state 函数

    函数原型 :int pthread_setcancelstate(int state, int *oldstate)

    作用 :设置本线程对cancel信号的反应,state有两种值,分别表示收到     信号后设为canceled状态和忽略cancel信号继续运行;oldstate如果     不为null则存入原来的cancel状态以便恢复。

    四、程序所用数据结构简介

    int num_producer ,num_consumer    :宏定义中生产者,消费者的数量

    int BUFFER_SIZE        :缓冲区大小

    int NUM        :产品编号

    int total_producer,total_consumer        :每5秒对生产者和消费者数量进行统计

    int buffer[BUFFER_SIZE]        :缓冲区模型

    int nextp, nextc        :生产者,消费者的指针

    pthread_t thrreads_p[100],threads_c[100]  :生产者,消费者线程

    sem_t empty,full, mutex  :信号量,以使生产者之间和消费者之间,以及二者之间的互斥

    void handler()         :用来循环输出生产者,消费者的数量

    五、程序源代码

    见附录B

    六、调试方案及调试过程记录与分析

    输入文件选取:

    本程序无需输入,只需按照需要更改生产者,消费者,缓冲区的数量和大小即可。

    测试结果、调试过程:

    第一次测试结果:显示很多函数文件未定义。

    第一次调试:在makefile文件开头加vpath命令,来指明用户自己所定义的头文件的位置,同时在每个用到用户自己定义头文件的.c文件的gcc-c一行加上-I./include

    第二次测试结果:头文件被包含进去,但NUM变量出现重复定义错误。

    第二次调试:在.h文件中声明全局变量,而全局变量的赋值放在了主函数producer_consumer.c中。

    第三次测试结果:使用1个生产者,1个消费者,2块缓冲区,此时无语法错误,但是改变之前的数据为3,3,20之后,生产者和消费者出现次序并没有体现同步与互斥。

    第三次调试:使用函数pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL)取消线程,和srand函数,使生产者和消费者可以同步和互斥。

    第四次测试结果:程序可以正确执行,无语法错误,并且可以实现同步和互斥。

    七、程序运行结果分析

    通过四次测试调试运行,程序可以正确执行,可以实现生产者、消费者的同步与互斥,以及对缓冲区的互斥使用。


    程序源代码:

     

    源文件名:producer_consumer.c
    内容:
    /* 
    ************************************************************************************** 
    *Project/File :producer_consumer.c 
    *By :
    *Mail :
    *Status :finished 
    *version :2.0 
    *Created Time :2014年09月24日星期三17时45分50秒 
    ************************************************************************************** 
    *Note: 
    *系统里有若干个合作的进程/线程,其中n个生产者线程,m个消费者线程,以及p块缓冲区。 
    *任何一个生产者都可以将自己的产品存入缓冲区的任何一个位置; 
    *任何一个消费者都可以将缓冲区内的一个产品取出; 
    *生产者源源不断地生产并存入产品; 
    *消费者周而复始地从缓冲区中取出产品将其消费掉; 
    ************************************************************************************** 
    */ 
     
    #include"producer_consumer.h" 
     
    void handler() 
    { 
    printf("total_producer = %d, total_consumer = %d\n", total_producer, total_consumer); 
    signal(SIGALRM, handler);  //让内核做好准备,一旦接受到SIGALARM信号,就执行 handler; 
    alarm(5); //闹钟设为5秒; 
     
    } 
     
    int main() 
    { 
    NUM=1; 
    total_producer = 0; 
    total_consumer = 0; 
    nextp=0; 
    nextc=0; 
    int p1; 
    p1 = fork(); 
    if(p1) 
    { 
    //parent 
    printf("This is parent(%d)!\n", getpid()); 
    } 
    else 
    { 
    //child 
    int i; 
    handler(); //每隔5秒钟统计一下生产的产品数和消费的产品数; 
    sem_init(&empty,0,BUFFER_SIZE);  //初始化信号量; 
    sem_init(&full,0,0); 
    sem_init(&mutex,0,1); 
     
    for(i=0;i<BUFFER_SIZE;i++) //清空缓冲区; 
    buffer[i]=0; 
    for(i=0;i<num_producer;i++)  //创建生产者,消费者线程; 
    pthread_create(&threads_p[i],NULL,producer_thread,(void *)(i+1)); 
    for(i=0;i<num_consumer;i++) 
    pthread_create(&threads_c[i],NULL,consumer_thread,(void *)(i+1)); 
     
    for(i=0;i<num_producer;i++)  //销毁线程; 
    pthread_join(threads_p[i],NULL); 
    for(i=0;i<num_consumer;i++) 
    pthread_join(threads_c[i],NULL); 
     
    sem_destroy(&full);   
    sem_destroy(&empty); //销毁信号量; 
    sem_destroy(&mutex); 
    } 
     
    return 0; 
    } 
     
     
     
    源文件名:producer.c
    内容:
    /* 
    ************************************************************************************** 
    *Project/File :producer.c 
    *By :
    *Mail : 
    *Status :finished 
    *version :2.0 
    *Created Time :2014年09月24日星期三17时45分50秒 
    ************************************************************************************** 
    *Note: 
    *当缓冲区中有空闲位置时,允许任何一个生产者把它的产品存入; 
    *当缓冲区中无空闲位置时,试图将产品存入缓冲区的任何生产者必须等待; 
    ************************************************************************************** 
    */ 
     
    #include"producer_consumer.h" 
     
    void *producer_thread(void *tid) 
    { 
    int i; 
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL); 
    while(1) 
    { 
    sem_wait(&empty); //信号量减一,即看看生产者是否可以向当前指针指向的位置放产品, 
    //保证生产者之间互斥; 
    srand((int)time(NULL) * (int)tid); 
    sleep(1); 
    sem_wait(&mutex); //信号量减一,看看当前时刻缓冲区是否被占用, 
    //保证生产者和消费者之间互斥; 
    // srand((int)time(NULL) * (int)tid); 
    if(NUM>BUFFER_SIZE) NUM=1; //如果大于20,NUM重新为1 ; 
    buffer[nextp]=(NUM++); 
    printf("生产者编号:%d\t指针:%d\t\n", (int)tid, nextp); 
    nextp=(nextp+1) % BUFFER_SIZE;  //生产者指针后移,指向下一个位置 
    total_producer++; 
    printf("缓冲区:"); 
    for(i=0;i<BUFFER_SIZE;i++) 
    { 
    printf("%3d",buffer[i]); 
    } 
    printf("\n"); 
     
    sem_post(&mutex); //释放缓冲区; 
    sem_post(&full); //告诉消费者有产品可以消费; 
    srand((int)time(NULL) * (int)tid); 
    } 
    return 0;  
    } 
     
     
    源文件名:consumer.c
    内容:
    /* 
    ************************************************************************************** 
    *Project/File :consumer.c 
    *By : 
    *Mail :
    *Status :finished 
    *version :2.0 
    *Created Time :2014年09月24日星期三17时45分50秒 
    ************************************************************************************** 
    *Note: 
    *当缓冲区中尚有未取出的产品时,允许任何一个消费者吧其中的一个产品取出; 
    *当缓冲区中没有未取出的产品时,试图从该环内取出产品的任何消费者必须等待; 
    ************************************************************************************** 
    */ 
     
    #include"producer_consumer.h" 
     
    void *consumer_thread(void *tid) 
    { 
    int i; 
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL); 
    while(1) 
    { 
    sem_wait(&full); // 从信号量减去一个"1"; 
    srand((int)time(NULL) * (int)tid); 
    sem_wait(&mutex); //对缓冲区互斥使用; 
     
    // srand((int)time(NULL) * (int)tid); 
    printf("消费者编号:%d\t  指针:%d\t\n",(int)tid,nextc); 
    buffer[nextc] = 0; 
    nextc =(nextc + 1) % BUFFER_SIZE; //指针后移,指向下一个产品; 
    total_consumer++;  
    sleep(1);  
    printf("缓冲区:"); 
    for(i = 0;i < BUFFER_SIZE;i++) 
    { 
    printf("%3d",buffer[i]); 
    } 
    printf("\n");  
     
    sem_post(&mutex); //释放缓冲区;  
    sem_post(&empty); //信号量加"1",告诉生产者可以放产品;  
    srand((int)time(NULL) * (int)tid); 
    } 
    return 0; 
    }
    文件名:producer_consumer.h
    内容:
    /* 
    ************************************************************************************** 
    *Project/File :producer_consumer.h 
    *By :
    *Mail :
    *Status :finished 
    *version :2.0 
    *Created Time :2014年09月24日星期三17时45分50秒 
    ************************************************************************************** 
    *Note: 
    *将所有.c文件所用到的头文件包含进去; 
    *包含所有的宏定义; 
    *全局变量的定义; 
    *函数的声明; 
    ************************************************************************************** 
    */ 
    #ifndef producer_consumer_h 
    #define producder_consumer_h 
     
    #include "stdio.h" 
    #include "stdlib.h" 
    #include "string.h" 
    #include "pthread.h" 
    #include "semaphore.h" 
    #include "signal.h" 
    #include "unistd.h" 
    #define num_producer 5 
    #define num_consumer 5 
    #define BUFFER_SIZE 20 
    int NUM; 
    int total_producer, total_consumer; 
    int buffer[BUFFER_SIZE]; 
    int nextp, nextc; 
    pthread_t threads_p[100], threads_c[100]; 
    sem_t empty, full, mutex; 
     
    void *producer_thread(void *tid); 
    void *consumer_thread(void *tid); 
     
    #endif
    文件名:Makefile
    内容:
    vpath %.h../producer_consumer 
    producer_consumer:producer.o consumer.o producer_consumer.o 
    gcc -o producer_consumer -pthread producer.o consumer.o producer_consumer.o 
    producer_consumer.o:producer_consumer.c producer_consumer.h 
    gcc -c producer_consumer.c -I./include 
    producer.o:producer.c producer_consumer.h 
    gcc -c producer.c -I./include 
    consumer.o:consumer.c producer_consumer.h 
    gcc -c consumer.c -I./include 
    


    展开全文
  • 高级编程知识点总结
  • linux进程通讯和线程同步知识

    千次阅读 2015-11-23 20:27:20
    linux进程通讯和线程同步笔记主要参考《unix高级环境编程》,主要对进程和线程的部分的常用函数进行说明和总结。linux进程通讯和线程同步 进程控制 子进程退出状态的查询 进程通信-管道 匿名管道 进程通讯-XSI信号量...

    linux进程通讯和线程同步

    笔记主要参考《unix高级环境编程》,主要对进程和线程的部分的常用函数进行说明和总结。

    进程控制

    使用fork可以创建子进程,fork返回0为子进程,返回值大于0为父进程,子进程可以使用getppid来获得父进程的进程ID。若要编写守护进程( daemon),编写守护进程的步骤:
    (1)在父进程中执行fork并exit推出;
    (2)在子进程中调用setsid函数创建新的会话;
    (3)在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;
    (4)在子进程中调用umask函数,设置进程的umask为0;
    (5)在子进程中关闭任何不需要的文件描述符

    子进程退出状态的查询

    相关函数:

    *取得子进程终止状态函数,回收资源
    #include <sys/wait.h>
    pid_t wait(int *statloc);
    pid_t waitpid(pid_t pid, int *statloc, int options);
    *取得进程终止状态函数
    #include <sys/wait.h>
    int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
    *内核返回的终止进程及其所有子进程使用资源
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <sys/time.h>
    #include <sys/resource.h>
    pid_t wait3(int *statloc, int options, struct rusage *rusage);
    pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);

    在UNIX 系统中,一个进程结束了,但是他的父进程没有等待(调用wait / waitpid)他, 那么他将变成一个僵尸进程. 但是如果该进程的父进程已经先结束了,那么该进程就不会变成僵尸进程, 因为每个进程结束的时候,系统都会扫描当前系统中所运行的所有进程, 看有没有哪个进程是刚刚结束的这个进程的子进程,如果是的话,就由Init 来接管他,成为他的父进程。如何查看僵尸进程: $ ps -el 其中,有标记为Z的进程就是僵尸进程 S代表休眠状态;D代表不可中断的休眠状态;R代表运行状态;Z代表僵死状态;T代表停止或跟踪状态。
    僵尸进程的避免
    1、父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起;
    2. 如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后, 父进程会收到该信号,可以在handler中调用wait回收 ;
    3. 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收, 并不再给父进程发送信号 ;
    4. 还有一些技巧,就是fork两次,父进程fork一个子进程,然后继续工作,子进程fork一 个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收 还要自己做。

    服务器采用了fork的话,要收集垃圾进程,防止僵死进程的产生,可以这样处理:signal(SIGCHLD,SIG_IGN); 交给系统init去回收。这里子进程就不会产生僵死进程了。

    void sig_chld(int signo) 
    { 
           pid_t   pid; 
           int     stat; 
           while((pid = waitpid(-1, &stat, WNOHANG)) > 0){ 
                   printf("child %d terminated\n", pid); 
           } 
            return; 

    进程通信-管道

    管道是单向的、先进先出的。它将一个程序的输入和另一个程序的输出连接起来。数据被一个进程读出后,将被从管道中删除。分为匿名和有名管道两种。前者用于父进程和子进程间的通信,后者用于同一系统的两个进程间通信。管道一般是半双工的(数据只能在一个方向流动)。

    匿名管道

    #include <unistd.h>
    int pipe(int fd[2]);
    其中,fd[0]用于读管道,fd[1]用于写管道。若成功则返回零,否则返回-1,错误原因存于errno中。

     int main(void)
    {
        int n;
        int fd[2];
        pid_t pid;
        char line[MAXLINE];
        if (pipe(fd) < 0)
           err_sys("pipe error");
        if ((pid = fork()) < 0) {
            err_sys("fork error");
        } else if (pid > 0) {   /* parent */
            close(fd[0]);
            write(fd[1], "hello world\n", 12);
        } else {     /* child */
            close(fd[1]);
            n = read(fd[0], line, MAXLINE);
            write(STDOUT_FILENO, line, n);
        }
        exit(0);
    }

    #### 使用命名管道

    命名管道也被称为FIFO文件,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但是它的行为却和之前所讲的没有名字的管道(匿名管道)类似。由于Linux中所有的事物都可被视为文件,所以对命名管道的使用也就变得与文件操作非常的统一,也使它的使用非常方便,同时我们也可以像平常的文件名一样在命令中使用。
    #include <sys/types.h>
    #include <sys/stat.h>
    int mkfifo(const char *filename, mode_t mode);
    int mkfifoat(int fd, const char *path, mode_t mode);
    int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t)0);
    与打开其他文件一样,FIFO文件也可以使用open调用来打开。注意,mkfifo函数只是创建一个FIFO文件,要使用命名管道还是将其打开。
    参考Linux进程间通信——使用命名管道
    程序一:

    #include <unistd.h>
    #include <stdlib.h>
    #include <fcntl.h>
    #include <limits.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <stdio.h>
    #include <string.h>
    
    int main()
    {
        const char *fifo_name = "/tmp/my_fifo";
        int pipe_fd = -1;
        int data_fd = -1;
        int res = 0;
        const int open_mode = O_WRONLY;
        int bytes_sent = 0;
        char buffer[PIPE_BUF + 1];
    
        if(access(fifo_name, F_OK) == -1)
        {
            //管道文件不存在
            //创建命名管道
            res = mkfifo(fifo_name, 0777);
            if(res != 0)
            {
                fprintf(stderr, "Could not create fifo %s\n", fifo_name);
                exit(EXIT_FAILURE);
            }
        }
    
        printf("Process %d opening FIFO O_WRONLY\n", getpid());
        //以只写阻塞方式打开FIFO文件,以只读方式打开数据文件
        pipe_fd = open(fifo_name, open_mode);
        data_fd = open("Data.txt", O_RDONLY);
        printf("Process %d result %d\n", getpid(), pipe_fd);
    
        if(pipe_fd != -1)
        {
            int bytes_read = 0;
            //向数据文件读取数据
            bytes_read = read(data_fd, buffer, PIPE_BUF);
            buffer[bytes_read] = '\0';
            while(bytes_read > 0)
            {
                //向FIFO文件写数据
                res = write(pipe_fd, buffer, bytes_read);
                if(res == -1)
                {
                    fprintf(stderr, "Write error on pipe\n");
                    exit(EXIT_FAILURE);
                }
                //累加写的字节数,并继续读取数据
                bytes_sent += res;
                bytes_read = read(data_fd, buffer, PIPE_BUF);
                buffer[bytes_read] = '\0';
            }
            close(pipe_fd);
            close(data_fd);
        }
        else
            exit(EXIT_FAILURE);
    
        printf("Process %d finished\n", getpid());
        exit(EXIT_SUCCESS);
    }

    程序二:

    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <fcntl.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <limits.h>
    #include <string.h>
    
    int main()
    {
        const char *fifo_name = "/tmp/my_fifo";
        int pipe_fd = -1;
        int data_fd = -1;
        int res = 0;
        int open_mode = O_RDONLY;
        char buffer[PIPE_BUF + 1];
        int bytes_read = 0;
        int bytes_write = 0;
        //清空缓冲数组
        memset(buffer, '\0', sizeof(buffer));
    
        printf("Process %d opening FIFO O_RDONLY\n", getpid());
        //以只读阻塞方式打开管道文件,注意与fifowrite.c文件中的FIFO同名
        pipe_fd = open(fifo_name, open_mode);
        //以只写方式创建保存数据的文件
        data_fd = open("DataFormFIFO.txt", O_WRONLY|O_CREAT, 0644);
        printf("Process %d result %d\n",getpid(), pipe_fd);
    
        if(pipe_fd != -1)
        {
            do
            {
                //读取FIFO中的数据,并把它保存在文件DataFormFIFO.txt文件中
                res = read(pipe_fd, buffer, PIPE_BUF);
                bytes_write = write(data_fd, buffer, res);
                bytes_read += res;
            }while(res > 0);
            close(pipe_fd);
            close(data_fd);
        }
        else
            exit(EXIT_FAILURE);
    
        printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);
        exit(EXIT_SUCCESS);
    }

    进程通讯-XSI信号量

    frok可以使用一个路径和项目ID(项目ID是0~255之间的字符值)产生一个IPC结构的键,键的数据结构为key_t;
    #include <sys/ipc.h>
    key_t ftok(const char *path, int id);

    信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。
    semget函数
    它的作用是创建一个新信号量或取得一个已有信号量,当我们使用XSI信号量时,首先需要调用semget来获得一个ID。原型为:
    #include <sys/sem.h>
    int semget(key_t key, int num_sems, int sem_flags);
    第二个参数num_sems指定需要的信号量数目,一个信号时它的值几乎总是1。
    semop函数
    它的作用是改变信号量的值,原型为:
    #include <sys/sem.h>
    int semop(int semid, struct sembuf semoparray[], size_t nops);
    sem_id是由semget返回的信号量标识符,sembuf结构的定义如下:

    struct sembuf{
        short sem_num;//除非使用一组信号量,否则它为0
        short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
                        //一个是+1,即V(发送信号)操作。
        short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
                        //并在进程没有释放该信号量而终止时,操作系统释放信号量
    };

    semctl函数
    该函数用来直接控制信号量信息,它的原型为:
    #include <sys/sem.h>
    int semctl(int semid, int semnum, int cmd, ... /* union semun arg */ );
    第四个参数是可选的,取决于第三个参数cmd。
    具体示例参照例程Linux进程间通信——使用信号量具体代码如下:

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #include <sys/sem.h>
    
    union semun
    {
        int val;
        struct semid_ds *buf;
        unsigned short *arry;
    };
    
    static int sem_id = 0;
    
    static int set_semvalue();
    static void del_semvalue();
    static int semaphore_p();
    static int semaphore_v();
    
    int main(int argc, char *argv[])
    {
        char message = 'X';
        int i = 0;
    
        //创建信号量
        sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
    
        if(argc > 1)
        {
            //程序第一次被调用,初始化信号量
            if(!set_semvalue())
            {
                fprintf(stderr, "Failed to initialize semaphore\n");
                exit(EXIT_FAILURE);
            }
            //设置要输出到屏幕中的信息,即其参数的第一个字符
            message = argv[1][0];
            sleep(2);
        }
        for(i = 0; i < 10; ++i)
        {
            //进入临界区
            if(!semaphore_p())
                exit(EXIT_FAILURE);
            //向屏幕中输出数据
            printf("%c", message);
            //清理缓冲区,然后休眠随机时间
            fflush(stdout);
            sleep(rand() % 3);
            //离开临界区前再一次向屏幕输出数据
            printf("%c", message);
            fflush(stdout);
            //离开临界区,休眠随机时间后继续循环
            if(!semaphore_v())
                exit(EXIT_FAILURE);
            sleep(rand() % 2);
        }
    
        sleep(10);
        printf("\n%d - finished\n", getpid());
    
        if(argc > 1)
        {
            //如果程序是第一次被调用,则在退出前删除信号量
            sleep(3);
            del_semvalue();
        }
        exit(EXIT_SUCCESS);
    }
    
    static int set_semvalue()
    {
        //用于初始化信号量,在使用信号量前必须这样做
        union semun sem_union;
    
        sem_union.val = 1;
        if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
            return 0;
        return 1;
    }
    
    static void del_semvalue()
    {
        //删除信号量
        union semun sem_union;
    
        if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
            fprintf(stderr, "Failed to delete semaphore\n");
    }
    
    static int semaphore_p()
    {
        //对信号量做减1操作,即等待P(sv)
        struct sembuf sem_b;
        sem_b.sem_num = 0;
        sem_b.sem_op = -1;//P()
        sem_b.sem_flg = SEM_UNDO;
        if(semop(sem_id, &sem_b, 1) == -1)
        {
            fprintf(stderr, "semaphore_p failed\n");
            return 0;
        }
        return 1;
    }
    
    static int semaphore_v()
    {
        //这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
        struct sembuf sem_b;
        sem_b.sem_num = 0;
        sem_b.sem_op = 1;//V()
        sem_b.sem_flg = SEM_UNDO;
        if(semop(sem_id, &sem_b, 1) == -1)
        {
            fprintf(stderr, "semaphore_v failed\n");
            return 0;
        }
        return 1;
    }

    使用信号量集的例程参见:linux进程间通信-信号量(semaphore) 信号量集合的例子

    进程通讯 — 共享内存(存储)

    顾名思义,共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。
    shmget函数
    调用调用shmget,获得一个共享存储标识符:
    #include <sys/shm.h>
    int shmget(key_t key, size_t size, int flag);
    第二个参数,size以字节为单位指定需要共享的内存容量;
    第三个参数,shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。
    shmat函数
    第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。
    #include <sys/shm.h>
    void *shmat(int shmid, const void *addr, int flag);
    shmdt函数
    该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。它的原型如下:
    #include <sys/shm.h>
    int shmdt(const void *addr);
    参数shmaddr是shmat函数返回的地址指针;
    shmctl函数
    与信号量的semctl函数一样,用来控制共享内存:
    #include <sys/shm.h>
    void *shmat(int shmid, const void *addr, int flag);
    第一个参数,shm_id是shmget函数返回的共享内存标识符。
    第二个参数,command是要采取的操作,它可以取下面的三个值 :
    IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
    IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
    IPC_RMID:删除共享内存段
    第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。
    shmid_ds结构至少包括以下成员:
    struct shmid_ds
    {
    uid_t shm_perm.uid;
    uid_t shm_perm.gid;
    mode_t shm_perm.mode;
    };
    具体的详细内容参考:Unix高级环境编程。
    具体代码参考:

    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <sys/shm.h>
    #include "shmdata.h"
    
    #define TEXT_SZ 2048
    struct shared_use_st
    {
        int written;//作为一个标志,非0:表示可读,0表示可写
        char text[TEXT_SZ];//记录写入和读取的文本
    };
    
    int main()
    {
        int running = 1;//程序是否继续运行的标志
        void *shm = NULL;//分配的共享内存的原始首地址
        struct shared_use_st *shared;//指向shm
        int shmid;//共享内存标识符
        //创建共享内存
        shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
        if(shmid == -1)
        {
            fprintf(stderr, "shmget failed\n");
            exit(EXIT_FAILURE);
        }
        //将共享内存连接到当前进程的地址空间
        shm = shmat(shmid, 0, 0);
        if(shm == (void*)-1)
        {
            fprintf(stderr, "shmat failed\n");
            exit(EXIT_FAILURE);
        }
        printf("\nMemory attached at %X\n", (int)shm);
        //设置共享内存
        shared = (struct shared_use_st*)shm;
        shared->written = 0;
        while(running)//读取共享内存中的数据
        {
            //没有进程向共享内存定数据有数据可读取
            if(shared->written != 0)
            {
                printf("You wrote: %s", shared->text);
                sleep(rand() % 3);
                //读取完数据,设置written使共享内存段可写
                shared->written = 0;
                //输入了end,退出循环(程序)
                if(strncmp(shared->text, "end", 3) == 0)
                    running = 0;
            }
            else//有其他进程在写数据,不能读取数据
                sleep(1);
        }
        //把共享内存从当前进程中分离
        if(shmdt(shm) == -1)
        {
            fprintf(stderr, "shmdt failed\n");
            exit(EXIT_FAILURE);
        }
        //删除共享内存
        if(shmctl(shmid, IPC_RMID, 0) == -1)
        {
            fprintf(stderr, "shmctl(IPC_RMID) failed\n");
            exit(EXIT_FAILURE);
        }
        exit(EXIT_SUCCESS);
    }

    其他程序的内容与之前相似,具体如:Linux进程间通信——使用共享内存
    进程间的同步可以使用共享内存中的互斥量来实现。若使用mutex互斥量,需要所有进程将相同文件映射到它们的地址空间里,并且使用PTHREAD_PROCESS_SHARED互斥属性在文件的相同偏移处初始化互斥量。

    进程通讯-文件记录锁

    记录锁(record locking)的功能是:当一个进程正在读或者修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区域。
    函数 flock()锁住整个文件
    flock()会依参数operation所指定的方式对参数fd所指的文件做各种锁定或解除锁定的动作。此函数只能锁定整个文件,无法锁定文件的某一区域。
    #include<sys/file.h>
    int flock(int fd,int operation);
    参数 operation有下列四种情况:
      LOCK_SH 建立共享锁定。多个进程可同时对同一个文件作共享锁定。
      LOCK_EX 建立互斥锁定。一个文件同时只有一个互斥锁定。
      LOCK_UN 解除文件锁定状态。
      LOCK_NB 无法建立锁定时,此操作可不被阻断,马上返回进程。通常与LOCK_SH或LOCK_EX 做OR(|)组合。
      单一文件无法同时建立共享锁定和互斥锁定,而当使用dup()或fork()时文件描述词不会继承此种锁定。返回值 返回0表示成功,若有错误则返回-1,错误代码存于errno。

    记录锁控制函数fcnl()
    当一个进程正在读或修改文件的某个部分是,它可以阻止其他进程修改同一文件区。
    #include <fcntl.h>
    int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );
    参数:cmd = F_GETLK,测试能否建立一把锁;cmd = F_SETLK,设置锁;cmd = F_SETLKW, 阻塞设置一把锁。
    POSIX只定义fock结构中必须有以下的数据成员,具体实现可以增加:
    struct flock {
    short l_type; /* 锁的类型: F_RDLCK, F_WRLCK, F_UNLCK */
    short l_whence; /* 加锁的起始位置:SEEK_SET, SEEK_CUR, SEEK_END */
    off_t l_start; /* 加锁的起始偏移,相对于l_whence */
    off_t l_len; /* 上锁的字节数*/
    pid_t l_pid; /* 已经占用锁的PID(只对F_GETLK 命令有效) */
    //
    };

    前面我们说了记录锁相当于读写锁的一种扩展类型,记录锁和读写锁一样也有两种锁:共享读锁(F_RDLCK)和独占写锁(F_WRLCK)。在使用规则上和读写锁也基本一样:
    * 文件给定字节区间,多个进程可以有一把共享读锁,即允许多个进程以读模式访问该字节区;
    * 文件给定字节区间,只能有一个进程有一把独占写锁,即只允许有一个进程已写模式访问该字节区;
    * 文件给定字节区间,如果有一把或多把读锁,不能在该字节区再加写锁,同样,如果有一把写锁,不能再该字节区再加任何读写锁。

    本部分内容可参考:Linux进程同步之记录锁(fcntl)和(UNIX环境高级编程(第3版))第14章 14.3节记录锁相关内容。关于fcntl函数的其他的用法参考(UNIX环境高级编程(第3版))第3章3.14节内容。

    进程通讯+线程同步-POSIX信号量

    POSIX信号量 :进程间通信中使用的XSI信号量的概念是一样,它是一种特殊的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作。POSIX信号量用在线程通信中。如果一个程序中有多个线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。而只有0和1两种取值的信号量叫做二进制信号量,在这里将重点介绍。而信号量一般常用于保护一段代码,使其每次只被一个执行线程运行。我们可以使用二进制信号量来完成这个工作。包含#include <semaphore.h>库即可使用下面的函数。
    创建函数sem_init/sem_open
    该函数用于创建信号量(作用相同),其原型如下:
    在单个进程中建议未命名:int sem_init(sem_t *sem, int pshared, unsigned int value);
    多个进程中建议命名:sem_t *sem_open(const char *name, int oflag, ... /* mode_t mode,unsigned int value */ );
    可以使用sem_unlink来销毁一个命名信号量:int sem_unlink(const char *name);。如果信号量没有打开,则该信号量被销毁;否则,销毁延迟到最后一个打开的引用关闭。在单线程中,使用sem_open打开的后立即使用sem_unlink销毁命名。销毁了这个名字,其他进程就再也不能访问它,也简化了清理工作

    sem_wait函数
    该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1,它们之间不会互相干扰。它的原型如下
    int sem_wait(sem_t *sem);
    sem_post函数
    该函数用于以原子操作的方式将信号量的值加1。它的原型如下:
    int sem_post(sem_t *sem);
    sem_destroy函数
    该函数用于对用完的信号量的清理。它的原型如下
    int sem_destroy(sem_t *sem);

    EINTR对POSIX信号量的影响(sem_wait、 EINTR)

    sem_wai和sem_timedwait如果超时值已经超过了调用规定的值,那么信号量不能被立即锁定,之后sem_timedwait() 为超时失败(error设置为ETIMEDOUT).
    所有的函数成功返回0,错误的话信号量的值不改动,返回-1.errno设定来标识错误.
    EINTR The call was interrupted by a signal handler; see signal(7).
      //调用被信号处理中断
    应该使用
      while ((s = sem_timedwait(&sem, &ts)) == -1 && errno == EINTR)
    或者是:
      while((rv = sem_wait(&Poll_IN)) != 0 && (errno == EINTR))
    的方式;

    进程通讯+线程同步-socket套接字

    进程控制

    从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位;线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。
    “进程——资源分配的最小单位,线程——程序执行的最小单位”
    进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

    线程退出状态的查询

    相关函数:

    #include <pthread.h>

    nt pthread_equal(pthread_t tid1, pthread_t tid2);/是否相等
    pthread_t pthread_self(void);/获得自身线程ID

    int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict attr,void *(*start_rtn)(void *), void *restrict arg); /创建线程

    void pthread_exit(void *rval_ptr);/线程结束
    int pthread_join(pthread_t thread, void **rval_ptr); /等待其他线程结束,阻塞的的函数
    int pthread_cancel(pthread_t tid); /请求取消同一进程中的其他线程

    void pthread_cleanup_push(void (*rtn)(void *), void *arg);
    void pthread_cleanup_pop(int execute); /线程清理处理程序

    int pthread_detach(pthread_t tid); /分离线程
    如果线程分离,底层的储存资源会在线程终止时立即被回收,而不用调用pthread_join进行回收。

    线程同步-互斥量

    互斥量从本质上说就是一把锁, 提供对共享资源的保护访问。一种用于多线程中的同步访问方法,它允许程序锁住某个对象,使得每次只能有一个线程访问它。
    相关函数:

     #include <pthread.h>
    int pthread_mutex_init(pthread_mutex_t *mutex, 
                            const pthread_mutexattr_t *mutexattr);
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    int pthread_mutex_destroy(pthread_mutex_t *mutex);

    进程间使用互斥量
    可以使用pthread_mutexattr_getpshared查询pthread_mutexattr_t结构,得到它的进程共享属性,使用pthread_mutexattr_setpshared寒素修改进程共享属性。参见本文档 进程通讯-共享内存(存储)最后一段,谈到进程中互斥量的使用方法。

    #include <pthread.h>
    int pthread_mutexattr_init(pthread_mutexattr_t *attr);
    int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
    
    int pthread_mutexattr_getpshared(const pthread_mutexattr_t * restrict attr, int *restrict pshared);
    int pthread_mutexattr_setpshared(pthread_mutexattr *attr,int pshared);

    规定互斥量语句允许绑定线程的阻塞时间用函数: pthread_mutex_timedlock来实现,超时 pthread_mutex_timedlock不会对互斥量进行加锁,而是返回错误码(number on failure)ETIMEDOUT;

    #include <pthread.h>
    #include <time.h>
    int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
                                const struct timespec *restrict tsptr);
    Returns: 0 if OK, error number on failure

    线程同步-读写锁

    相关函数:

    #include <pthread.h>
    int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                            const pthread_rwlockattr_t *restrict attr);
    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
    
    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
    
    int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
    
    int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
                                const struct timespec *restrict tsptr);
    int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
                                const struct timespec *restrict tsptr);

    线程同步-条件变量

    相关函数

    #include <pthread.h>
    int pthread_cond_init(pthread_cond_t *restrict cond,
                        const pthread_condattr_t *restrict attr);
    int pthread_cond_destroy(pthread_cond_t *cond);
    
    int pthread_cond_wait(pthread_cond_t *restrict cond,
                        pthread_mutex_t *restrict mutex);
    int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                        pthread_mutex_t *restrict mutex,
                        const struct timespec *restrict tsptr);
    
    int pthread_cond_signal(pthread_cond_t *cond);
    int pthread_cond_broadcast(pthread_cond_t *cond);

    线程同步-屏障(barrier)

    屏障允许任意数量的线程等待,直到所有的线程完成处理工作,而且线程不用退出。所有的线程到达屏障后可以继续工作。
    相关函数

    int pthread_barrier_init(pthread_barrier_t *restrict barrier,
                        const pthread_barrierattr_t *restrict attr,
                        unsigned int count);
    int pthread_barrier_destroy(pthread_barrier_t *barrier);
    
    int pthread_barrier_wait(pthread_barrier_t *barrier);

    调用pthread_barrier_wait阻塞,直到有个pthread_barrier_wait调用满足pthread_barrier_init规定的数量,所有的线程都被唤醒。

    线程和信号(sigwait 、EINTR)

    相关函数

    #include <signal.h>
    int pthread_sigmask(int how, const sigset_t *restrict set,
                        sigset_t *restrict oset);
    int sigwait(const sigset_t *restrict set, int *restrict signop);
    int pthread_kill(pthread_t thread, int signo);

    sigwait碰到的问题主要参考:多线程下慎用sigwait 。问题线程大致流程如下:

    void thread(void *data)
    {
        int wait_sig = *(int*)data;
        sigset_t sigset;
        sigemptyset(&sigset);
        sigaddset(&sigset, wait_ig);
        while (1) {
            int signal;
            if (0 != sigwait(&sigset, &signal)) {
                break;
            }
        }
    }

    出现的问题是这个断言失败,也就是说sigwait失败了。后来打印出sigwait的返回值,发现sigwait的失败的原因时EINTR,也就说 sigwait被一个信号中断了,但是不知道信号来自何处。说到此处,先说明一下,sigwait这个函数很奇怪,跟一般的linux API不同。sigwait出错的时候,并不设置errno,而直接把errno错误值返回。
    通过增加一个新的判断

    int ret = sigwait(&sigset, &signal));
    if (EINTR == ret) {
        continue;
    }
    else if (ret) {
        break;
    }

    毕竟sigwait作为一个阻塞操作,因为收到信号而失败,是可以接受的行为,所以要对EINTR进行特殊处理。
    在POSIX标准中,当进程收到信号时,如果是多线程的情况,我们是无法确定是哪一个线程处理这个信号。而sigwait是从进程中pending的信号中,取走指定的信号。这样的话,如果要确保sigwait这个线程收到该信号,那么所有线程含主线程以及这个sigwait线程则必须block(pthread_sigmask和sigpromask)住这个信号。否则如果在两次sigwait之间,收到了指定信号,该信号很有可能被任意一个线程处理掉。

    线程同步相关代码详见《Unix环境高级编程(第3版)》

    其他知识点

    进程的间的相互通讯和线程间的同步是Linux/Unix类操作系统编程中比较重要的部分。虽然Linux和unix操作系统都提供了上述提供的机制,不同的通讯和同步方法在不同的操作系统体现出的性能差别较大,必要时可以使用系统特性的功能呢和内核函数(如:Linux 中sendfile拷贝,Epoll及其LT和ET模式;BSD的Kqueue事件队列)。其他一些通讯机制如:XSI消息队列等些许机制使用较少,且某些情况存在软件移植性的问题,本文不做讨论;UNIX套接字域可以用于进程通讯和线程同步,这个部分基本与Socket网络的函数基本一样,故此不做探讨。进程和线程的运行是一个十分复杂的系统,线程和进程的其他相关部分知识点也较为复杂。如:权限(系统权限,用户权限,组权限),I/O(文件I/O,标准I/O,非阻塞I/O,I/O多路转接-select/poll/epoll,异步I/O-AIO),守护进程,终端I/O,为终端等各种运行换件。另外进程和线程的信号(signal)系统也有相当的技巧。

    展开全文
  • Linux进程线程的区别

    千次阅读 2019-12-12 16:28:34
    Linux进程线程的区别 ** 本文较长,耐心阅读,必有收获! 进程与线程的区别,早已经成为了经典问题。自线程概念诞生起,关于这个问题的讨论就没有停止过。无论是初级程序员,还是资深专家,都应该考虑过这个问题,...

    **

    Linux进程与线程的区别

    **

    本文较长,耐心阅读,必有收获!

    进程与线程的区别,早已经成为了经典问题。自线程概念诞生起,关于这个问题的讨论就没有停止过。无论是初级程序员,还是资深专家,都应该考虑过这个问题,只是层次角度不同罢了。一般程序员而言,搞清楚二者的概念,在工作实际中去运用成为了焦点。而资深工程师则在考虑系统层面如何实现两种技术及其各自的性能和实现代价。以至于到今天,Linux内核还在持续更新完善(关于进程和线程的实现模块也是内核完善的任务之一)。

    本文将以一个从事Linux平台系统开发的程序员角度描述这个经典问题。本文素材全部来源于工作实践经验与知识规整,若有疏漏或不正之处,敬请读者慷慨指出。

    0.首先,简要了解一下进程和线程。对于操作系统而言,进程是核心之核心,整个现代操作系统的根本,就是以进程为单位在执行任务。系统的管理架构也是基于进程层面的。在按下电源键之后,计算机就开始了复杂的启动过程,此处有一个经典问题:当按下电源键之后,计算机如何把自己由静止启动起来的?本文不讨论系统启动过程,请读者自行科普。操作系统启动的过程简直可以描述为上帝创造万物的过程,期初没有世界,但是有上帝,是上帝创造了世界,之后创造了万物,然后再创造了人,然后塑造了人的七情六欲,再然后人类社会开始遵循自然规律繁衍生息。。。操作系统启动进程的阶段就相当于上帝造人的阶段。本文讨论的全部内容都是“上帝造人”之后的事情。第一个被创造出来的进程是0号进程,这个进程在操作系统层面是不可见的,但它存在着。0号进程完成了操作系统的功能加载与初期设定,然后它创造了1号进程(init),这个1号进程就是操作系统的“耶稣”。1号进程是上帝派来管理整个操作系统的,所以在用pstree查看进程树可知,1号进程位于树根。再之后,系统的很多管理程序都以进程身份被1号进程创造出来,还创造了与人类沟通的桥梁——shell。从那之后,人类可以跟操作系统进行交流,可以编写程序,可以执行任务。。。

    而这一切,都是基于进程的。每一个任务(进程)被创建时,系统会为他分配存储空间等必要资源,然后在内核管理区为该进程创建管理节点,以便后来控制和调度该任务的执行。

    进程真正进入执行阶段,还需要获得CPU的使用权,这一切都是操作系统掌管着,也就是所谓的调度,在各种条件满足(资源与CPU使用权均获得)的情况下,启动进程的执行过程。

    除CPU而外,一个很重要的资源就是存储器了,系统会为每个进程分配独有的存储空间,当然包括它特别需要的别的资源,比如写入时外部设备是可使用状态等等。有了上面的引入,我们可以对进程做一个简要的总结:

    进程,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。它的执行需要系统分配资源创建实体之后,才能进行。

    随着技术发展,在执行一些细小任务时,本身无需分配单独资源时(多个任务共享同一组资源即可,比如所有子进程共享父进程的资源),进程的实现机制依然会繁琐的将资源分割,这样造成浪费,而且还消耗时间。后来就有了专门的多任务技术被创造出来——线程。

    线程的特点就是在不需要独立资源的情况下就可以运行。如此一来会极大节省资源开销,以及处理时间。

    1.好了,前面的一段文字是简要引入两个名词,即进程和线程。本文讨论目标是解释清楚进程和线程的区别,关于二者的技术实现,请读者查阅相关资料。

    下面我们开始重点讨论本文核心了。从下面几个方面阐述进程和线程的区别。

    1).二者的相同点

    2).实现方式的差异

    3).多任务程序设计模式的区别

    4).实体间(进程间,线程间,进线程间)通信方式的不同

    5).控制方式的异同

    6).资源管理方式的异同

    7).个体间辈分关系的迥异

    8).进程池与线程池的技术实现差别

    接下来我们就逐个进行解释。

    1).二者的相同点

    无论是进程还是线程,对于程序员而言,都是用来实现多任务并发的技术手段。二者都可以独立调度,因此在多任务环境下,功能上并无差异。并且二者都具有各自的实体,是系统独立管理的对象个体。所以在系统层面,都可以通过技术手段实现二者的控制。而且二者所具有的状态都非常相似。而且,在多任务程序中,子进程(子线程)的调度一般与父进程(父线程)平等竞争。

    其实在Linux内核2.4版以前,线程的实现和管理方式就是完全按照进程方式实现的。在2.6版内核以后才有了单独的线程实现。

    在这里插入图片描述
    在这里插入图片描述

    2).实现方式的差异

    进程是资源分配的基本单位,线程是调度的基本单位。

    这句经典名言已流传数十年,各种操作系统教材都可见此描述。确实如此,这就是二者的显著区别。读者请注意“基本”二字。相信有读者看到前半句的时候就在心里思考,“进程岂不是不能调度?”,非也!进程和线程都可以被调度,否则多进程程序该如何运行呢!

    只是,线程是更小的可以调度的单位,也就是说,只要达到线程的水平就可以被调度了,进程自然可以被调度。它强调的是分配资源时的对象必须是进程,不会给一个线程单独分配系统管理的资源。若要运行一个任务,想要获得资源,最起码得有进程,其他子任务可以以线程身份运行,资源共享就行了。

    简而言之,进程的个体间是完全独立的,而线程间是彼此依存的。多进程环境中,任何一个进程的终止,不会影响到其他进程。而多线程环境中,父线程终止,全部子线程被迫终止(没有了资源)。而任何一个子线程终止一般不会影响其他线程,除非子线程执行了exit()系统调用。任何一个子线程执行exit(),全部线程同时灭亡。

    其实,也没有人写出只有线程而没有进程的程序。多线程程序中至少有一个主线程,而这个主线程其实就是有main函数的进程。它是整个程序的进程,所有线程都是它的子线程。我们通常把具有多线程的主进程称之为主线程。

    从系统实现角度讲,进程的实现是调用fork系统调用:

    pid_t fork(void);

    线程的实现是调用clone系统调用:

    int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, …

    /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */

    );

    其中,fork()是将父进程的全部资源复制给了子进程。而线程的clone只是复制了一小部分必要的资源。在调用clone时可以通过参数控制要复制的对象。可以说,fork实现的是clone的加强完整版。当然,后来操作系统还进一步优化fork实现——写时复制技术。在子进程需要复制资源(比如子进程执行写入动作更改父进程内存空间)时才复制,否则创建子进程时先不复制。

    实际中,编写多进程程序时采用fork创建子进程实体。而创建线程时并不采用clone系统调用,而是采用线程库函数。常用线程库有Linux-Native线程库和POSIX线程库。其中应用最为广泛的是POSIX线程库。因此读者在多线程程序中看到的是pthread_create而非clone。

    我们知道,库是建立在操作系统层面上的功能集合,因而它的功能都是操作系统提供的。由此可知,线程库的内部很可能实现了clone的调用。不管是进程还是线程的实体,都是操作系统上运行的实体。

    最后,我们说一下vfork() 。这也是一个系统调用,用来创建一个新的进程。它创建的进程并不复制父进程的资源空间,而是共享,也就说实际上vfork实现的是一个接近线程的实体,只是以进程方式来管理它。并且,vfork()的子进程与父进程的运行时间是确定的:子进程“结束”后父进程才运行。请读者注意“结束”二字。并非子进程完成退出之意,而是子进程返回时。一般采用vfork()的子进程,都会紧接着执行execv启动一个全新的进程,该进程的进程空间与父进程完全独立不相干,所以不需要复制父进程资源空间。此时,execv返回时父进程就认为子进程“结束”了,自己开始运行。实际上子进程继续在一个完全独立的空间运行着。举个例子,比如在一个聊天程序中,弹出了一个视频播放器。你说视频播放器要继承你的聊天程序的进程空间的资源干嘛?莫非视频播放器想要窥探你的聊天隐私不成?懂了吧!

    3).多任务程序设计模式的区别

    由于进程间是独立的,所以在设计多进程程序时,需要做到资源独立管理时就有了天然优势,而线程就显得麻烦多了。比如多任务的TCP程序的服务端,父进程执行accept()一个客户端连接请求之后会返回一个新建立的连接的描述符DES,此时如果fork()一个子进程,将DES带入到子进程空间去处理该连接的请求,父进程继续accept等待别的客户端连接请求,这样设计非常简练,而且父进程可以用同一变量(val)保存accept()的返回值,因为子进程会复制val到自己空间,父进程再覆盖此前的值不影响子进程工作。但是如果换成多线程,父线程就不能复用一个变量val多次执行accept()了。因为子线程没有复制val的存储空间,而是使用父线程的,如果子线程在读取val时父线程接受了另一个客户端请求覆盖了该值,则子线程无法继续处理上一次的连接任务了。改进的办法是子线程立马复制val的值在自己的栈区,但父线程必须保证子线程复制动作完成之后再执行新的accept()。但这执行起来并不简单,因为子线程与父线程的调度是独立的,父线程无法知道子线程何时复制完毕。这又得发生线程间通信,子线程复制完成后主动通知父线程。这样一来父线程的处理动作必然不能连贯,比起多进程环境,父线程显得效率有所下降。

    PS:这里引述一个知名的面试问题:多进程的TCP服务端,能否互换fork()与accept()的位置?
    通过fork()创建子进程时,子进程继承父进程环境和上下文的大部分内容的拷贝,其中就包括文件描述符表。

    (1)对于父进程在fork()之前所建立的连接,子进程都会继承,与父进程共享相同的文件偏移量。系统文件表位于系统空间中,不会被fork()复制,但是系统文件表中的条目会保存指向它的文件描述符表的计数,

    fork()时需要对这个计数进行维护,以体现子进程对应的新的文件描述符表也指向它。程序关闭文件时,也是将系统文件表条目内部的计数减一,当计数值减为0时,将其删除。

    (2)对于父进程在fork()之后建立连接,此时还没有打开文件描述符,所以子进程没有继承到文件描述符,子进程将会自己建立一条连接,不与父进程共享偏移量,而此时父进程也会建立一条连接,并且文件描述符表中的计数器会增加,当子进程结束后,文件计数器减一,而父进程一直执行,但不会为零,所以这个文件描述符会一直存在,占用资源。

    所以在faccept之前fork()后要在父进程中关闭accept的描述符,并且在fork子进程中关闭listen的描述符;而在accept后调用fork()则只需要在子进程中关闭listen描述符,父进程中不做处理。

    关于资源不独立,看似是个缺点,但在有的情况下就成了优点。多进程环境间完全独立,要实现通信的话就得采用进程间的通信方式,它们通常都是耗时间的。而线程则不用任何手段数据就是共享的。当然多个子线程在同时执行写入操作时需要实现互斥,否则数据就写“脏”了。

    4).实体间(进程间,线程间,进线程间)通信方式的不同

    进程间的通信方式有这样几种:

    A.共享内存 B.消息队列 C.信号量 D.有名管道 E.无名管道 F.信号

    G.文件 H.socket

    线程间的通信方式上述进程间的方式都可沿用,且还有自己独特的几种:

    A.互斥量 B.自旋锁 C.条件变量 D.读写锁 E.线程信号

    G.全局变量

    值得注意的是,线程间通信用的信号不能采用进程间的信号,因为信号是基于进程为单位的,而线程是共属于同一进程空间的。故而要采用线程信号。

    综上,进程间通信手段有8种。线程间通信手段有13种。

    而且,进程间采用的通信方式要么需要切换内核上下文,要么要与外设访问(有名管道,文件)。所以速度会比较慢。而线程采用自己特有的通信方式的话,基本都在自己的进程空间内完成,不存在切换,所以通信速度会较快。也就是说,进程间与线程间分别采用的通信方式,除了种类的区别外,还有速度上的区别。

    另外,进程与线程之间穿插通信的方式,除信号以外其他进程间通信方式都可采用。
    线程有内核态线程与用户级线程,相关知识请参看我的另一篇博文《Linux线程的实质》。

    5).控制方式的异同

    进程与线程的身份标示ID管理方式不一样,进程的ID为pid_t类型,实际为一个int型的变量(也就是说是有限的):

    /usr/include/unistd.h:260:typedef __pid_t pid_t;

    /usr/include/bits/types.h:126:# define __STD_TYPE typedef

    /usr/include/bits/types.h:142:__STD_TYPE __PID_T_TYPE __pid_t;

    /usr/include/bits/typesizes.h:53:#define __PID_T_TYPE __S32_TYPE

    /usr/include/bits/types.h💯#define __S32_TYPE int

    在全系统中,进程ID是唯一标识,对于进程的管理都是通过PID来实现的。每创建一个进程,内核去中就会创建一个结构体来存储该进程的全部信息:

    注:下述代码来自 Linux内核3.18.1

    include/linux/sched.h:1235:struct task_struct {

        volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
    
        void *stack;
    

        pid_t pid;
    
        pid_t tgid;
    

    };

    每一个存储进程信息的节点也都保存着自己的PID。需要管理该进程时就通过这个ID来实现(比如发送信号)。当子进程结束要回收时(子进程调用exit()退出或代码执行完),需要通过wait()系统调用来进行,未回收的消亡进程会成为僵尸进程,其进程实体已经不复存在,但会虚占PID资源,因此回收是有必要的。

    线程的ID是一个long型变量:

    /usr/include/bits/pthreadtypes.h:60:typedef unsigned long int pthread_t;

    它的范围大得多,管理方式也不一样。线程ID一般在本进程空间内作用就可以了,当然系统在管理线程时也需要记录其信息。其方式是,在内核创建一个内核态线程与之对应,也就是说每一个用户创建的线程都有一个内核态线程对应。但这种对应关系不是一对一,而是多对一的关系,也就是一个内核态线程可以对应着多个用户级线程。还是请读者参看《Linux线程的实质》普及相关概念。此处贴出blog地址:

    http://my.oschina.net/cnyinlinux/blog/367910

    对于线程而言,若要主动终止需要调用pthread_exit() ,主线程需要调用pthread_join()来回收(前提是该线程没有被detached,相关概念请查阅线程的“分离属性”)。像线发送线程信号也是通过线程ID实现的。

    6).资源管理方式的异同

    进程本身是资源分配的基本单位,因而它的资源都是独立的,如果有多进程间的共享资源,就要用到进程间的通信方式了,比如共享内存。共享数据就放在共享内存去,大家都可以访问,为保证数据写入的安全,加上信号量一同使用。一般而言,共享内存都是和信号量一起使用。消息队列则不同,由于消息的收发是原子操作,因而自动实现了互斥,单独使用就是安全的。

    线程间要使用共享资源不需要用共享内存,直接使用全局变量即可,或者malloc()动态申请内存。显得方便直接。而且互斥使用的是同一进程空间内的互斥量,所以效率上也有优势。

    实际中,为了使程序内资源充分规整,也都采用共享内存来存储核心数据。不管进程还是线程,都采用这种方式。原因之一就是,共享内存是脱离进程的资源,如果进程发生意外终止的话,共享内存可以独立存在不会被回收(是否回收由用户编程实现)。进程的空间在进程崩溃的那一刻也被系统回收了。虽然有coredump机制,但也只能是有限的弥补。共享内存在进程down之后还完整保存,这样可以拿来分析程序的故障原因。同时,运行的宝贵数据没有丢失,程序重启之后还能继续处理之前未完成的任务,这也是采用共享内存的又一大好处。

    总结之,进程间的通信方式都是脱离于进程本身存在的,是全系统都可见的。这样一来,进程的单点故障并不会损毁数据,当然这不一定全是优点。比如,进程崩溃前对信号量加锁,崩溃后重启,然后再次进入运行状态,此时直接进行加锁,可能造成死锁,程序再也无法继续运转。再比如,共享内存是全系统可见的,如果你的进程资源被他人误读误写,后果肯定也是你不想要的。所以,各有利弊,关键在于程序设计时如何考量,技术上如何规避。这说起来又是编程技巧和经验的事情了。

    7).个体间辈分关系的迥异

    进程的备份关系森严,在父进程没有结束前,所有的子进程都尊从父子关系,也就是说A创建了B,则A与B是父子关系,B又创建了C,则B与C也是父子关系,A与C构成爷孙关系,也就是说C是A的孙子进程。在系统上使用pstree命令打印进程树,可以清晰看到备份关系。

    多线程间的关系没有那么严格,不管是父线程还是子线程创建了新的线程,都是共享父线程的资源,所以,都可以说是父线程的子线程,也就是只存在一个父线程,其余线程都是父线程的子线程。

    8).进程池与线程池的技术实现差别

    我们都知道,进程和线程的创建时需要时间的,并且系统所能承受的进程和线程数也是有上限的,这样一来,如果业务在运行中需要动态创建子进程或线程时,系统无法承受不能立即创建的话,必然影响业务。综上,聪明的程序员发明了一种新方法——池。

    在程序启动时,就预先创建一些子进程或线程,这样在需要用时直接使唤。这就是老人口中的“多生孩子多种树”。程序才开始运行,没有那么多的服务请求,必然大量的进程或线程空闲,这时候一般让他们“冬眠”,这样不耗资源,要不然一大堆孩子的口食也是个负担啊。对于进程和线程而言,方式是不一样的。另外,当你有了任务,要分配给那些孩子的时候,手段也不一样。下面就分别来解说。

    进程池

    首先创建了一批进程,就得管理,也就是你得分开保存进程ID,可以用数组,也可用链表。建议用数组,这样可以实现常数内找到某个线程,而且既然做了进程池,就预先估计好了生产多少进程合适,一般也不会再动态延展。就算要动态延展,也能预估范围,提前做一个足够大的数组。不为别的,就是为了快速响应。本来错进程池的目的也是为了效率。

    接下来就要让闲置进程冬眠了,可以让他们pause()挂起,也可用信号量挂起,还可以用IPC阻塞,方法很多,分析各自优缺点根据实际情况采用就是了。

    然后是分配任务了,当你有任务的时候就要让他干活了。唤醒了进程,让它从哪儿开始干呢?肯定得用到进程间通信了,比如信号唤醒它,然后让它在预先指定的地方去读取任务,可以用函数指针来实现,要让它干什么,就在约定的地方设置代码段指针。这也只是告诉了它怎么干,还没说干什么(数据条件),再通过共享内存把要处理的数据设置好,这也子进程就知道怎么做了。干完之后再来一次进程间通信然后自己继续冬眠,父进程就知道孩子干完了,收割成果。

    最后结束时回收子进程,向各进程发送信号唤醒,改变激活状态让其主动结束,然后逐个wait()就可以了。

    线程池

    线程池的思想与上述类似,只是它更为轻量级,所以调度起来不用等待额外的资源。

    要让线程阻塞,用条件变量就是了,需要干活的时候父线程改变条件,子线程就被激活。

    线程间通信方式就不用赘述了,不用繁琐的通信就能达成,比起进程间效率要高一些。

    线程干完之后自己再改变条件,这样父线程也就知道该收割成果了。

    整个程序结束时,逐个改变条件并改变激活状态让子线程结束,最后逐个回收即可。

    展开全文
  • linux内核知识系列:线程&进程 华嵌智能提供 www.embedded-cn.com http://embedded-cn.taobao.com
  • linux进程线程

    2013-11-12 13:38:02
    基础知识线程进程 按照教科书上的定义,进程是资源管理的最小单位,线程是程序执行的最小单位。在操作系统设计上,从进程演化出线程,最主要的目的就是更好的支持SMP以及减小(进程/线程)上下文切换开销。 ...

    一.基础知识:线程和进程

    按照教科书上的定义,进程是资源管理的最小单位,线程是程序执行的最小单位。在操作系统设计上,从进程演化出线程,最主要的目的就是更好的支持SMP以及减小(进程/线程)上下文切换开销。

    无论按照怎样的分法,一个进程至少需要一个线程作为它的指令执行体,进程管理着资源(比如cpu、内存、文件等等),而将线程分配到某个cpu上执行。一个进程当然可以拥有多个线程,此时,如果进程运行在SMP机器上,它就可以同时使用多个cpu来执行各个线程,达到最大程度的并行,以提高效率;同时,即使是在单cpu的机器上,采用多线程模型来设计程序,正如当年采用多进程模型代替单进程模型一样,使设计更简洁、功能更完备,程序的执行效率也更高,例如采用多个线程响应多个输入,而此时多线程模型所实现的功能实际上也可以用多进程模型来实现,而与后者相比,线程的上下文切换开销就比进程要小多了,从语义上来说,同时响应多个输入这样的功能,实际上就是共享了除cpu以外的所有资源的。

    针对线程模型的两大意义,分别开发出了核心级线程和用户级线程两种线程模型,分类的标准主要是线程的调度者在核内还是在核外。前者更利于并发使用多处理器的资源,而后者则更多考虑的是上下文切换开销。在目前的商用系统中,通常都将两者结合起来使用,既提供核心线程以满足smp系统的需要,也支持用线程库的方式在用户态实现另一套线程机制,此时一个核心线程同时成为多个用户态线程的调度者。正如很多技术一样,"混合"通常都能带来更高的效率,但同时也带来更大的实现难度,出于"简单"的设计思路,Linux从一开始就没有实现混合模型的计划,但它在实现上采用了另一种思路的"混合"。

    在线程机制的具体实现上,可以在操作系统内核上实现线程,也可以在核外实现,后者显然要求核内至少实现了进程,而前者则一般要求在核内同时也支持进程。核心级线程模型显然要求前者的支持,而用户级线程模型则不一定基于后者实现。这种差异,正如前所述,是两种分类方式的标准不同带来的。

    当核内既支持进程也支持线程时,就可以实现线程-进程的"多对多"模型,即一个进程的某个线程由核内调度,而同时它也可以作为用户级线程池的调度者,选择合适的用户级线程在其空间中运行。这就是前面提到的"混合"线程模型,既可满足多处理机系统的需要,也可以最大限度的减小调度开销。绝大多数商业操作系统(如Digital Unix、Solaris、Irix)都采用的这种能够完全实现POSIX1003.1c标准的线程模型。在核外实现的线程又可以分为"一对一"、"多对一"两种模型,前者用一个核心进程(也许是轻量进程)对应一个线程,将线程调度等同于进程调度,交给核心完成,而后者则完全在核外实现多线程,调度也在用户态完成。后者就是前面提到的单纯的用户级线程模型的实现方式,显然,这种核外的线程调度器实际上只需要完成线程运行栈的切换,调度开销非常小,但同时因为核心信号(无论是同步的还是异步的)都是以进程为单位的,因而无法定位到线程,所以这种实现方式不能用于多处理器系统,而这个需求正变得越来越大,因此,在现实中,纯用户级线程的实现,除算法研究目的以外,几乎已经消失了。

    Linux内核只提供了轻量进程的支持,限制了更高效的线程模型的实现,但Linux着重优化了进程的调度开销,一定程度上也弥补了这一缺陷。目前最流行的线程机制LinuxThreads所采用的就是线程-进程"一对一"模型,调度交给核心,而在用户级实现一个包括信号处理在内的线程管理机制。Linux-LinuxThreads的运行机制正是本文的描述重点。

    二.Linux 2.4内核中的轻量进程实现

    最初的进程定义都包含程序、资源及其执行三部分,其中程序通常指代码,资源在操作系统层面上通常包括内存资源、IO资源、信号处理等部分,而程序的执行通常理解为执行上下文,包括对cpu的占用,后来发展为线程。在线程概念出现以前,为了减小进程切换的开销,操作系统设计者逐渐修正进程的概念,逐渐允许将进程所占有的资源从其主体剥离出来,允许某些进程共享一部分资源,例如文件、信号,数据内存,甚至代码,这就发展出轻量进程的概念。Linux内核在2.0.x版本就已经实现了轻量进程,应用程序可以通过一个统一的clone()系统调用接口,用不同的参数指定创建轻量进程还是普通进程。在内核中,clone()调用经过参数传递和解释后会调用do_fork(),这个核内函数同时也是fork()、vfork()系统调用的最终实现:

    <linux-2.4.20/kernel/fork.c>
    int do_fork(unsigned long clone_flags, unsigned long stack_start, 
    struct pt_regs *regs, unsigned long stack_size)
    

    其中的clone_flags取自以下宏的"或"值:

    <linux-2.4.20/include/linux/sched.h>
    #define CSIGNAL      0x000000ff  
    /* signal mask to be sent at exit */
    #define CLONE_VM    0x00000100 
    /* set if VM shared between processes */
    #define CLONE_FS        0x00000200  
    /* set if fs info shared between processes */
    #define CLONE_FILES     0x00000400  
    /* set if open files shared between processes */
    #define CLONE_SIGHAND  0x00000800 
    /* set if signal handlers and blocked signals shared */
    #define CLONE_PID    0x00001000  
    /* set if pid shared */
    #define CLONE_PTRACE  0x00002000  
    /* set if we want to let tracing continue on the child too */
    #define CLONE_VFORK  0x00004000  
    /* set if the parent wants the child to wake it up on mm_release */
    #define CLONE_PARENT  0x00008000  
    /* set if we want to have the same parent as the cloner */
    #define CLONE_THREAD  0x00010000  
    /* Same thread group? */
    #define CLONE_NEWNS  0x00020000  /* New namespace group? */
    #define CLONE_SIGNAL   (CLONE_SIGHAND | CLONE_THREAD)
    

    在do_fork()中,不同的clone_flags将导致不同的行为,对于LinuxThreads,它使用(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND)参数来调用clone()创建"线程",表示共享内存、共享文件系统访问计数、共享文件描述符表,以及共享信号处理方式。本节就针对这几个参数,看看Linux内核是如何实现这些资源的共享的。

    1.CLONE_VM

    do_fork()需要调用copy_mm()来设置task_struct中的mm和active_mm项,这两个mm_struct数据与进程所关联的内存空间相对应。如果do_fork()时指定了CLONE_VM开关,copy_mm()将把新的task_struct中的mm和active_mm设置成与current的相同,同时提高该mm_struct的使用者数目(mm_struct::mm_users)。也就是说,轻量级进程与父进程共享内存地址空间,由下图示意可以看出mm_struct在进程中的地位:


     

    2.CLONE_FS

    task_struct中利用fs(struct fs_struct *)记录了进程所在文件系统的根目录和当前目录信息,do_fork()时调用copy_fs()复制了这个结构;而对于轻量级进程则仅增加fs->count计数,与父进程共享相同的fs_struct。也就是说,轻量级进程没有独立的文件系统相关的信息,进程中任何一个线程改变当前目录、根目录等信息都将直接影响到其他线程。

    3.CLONE_FILES

    一个进程可能打开了一些文件,在进程结构task_struct中利用files(struct files_struct *)来保存进程打开的文件结构(struct file)信息,do_fork()中调用了copy_files()来处理这个进程属性;轻量级进程与父进程是共享该结构的,copy_files()时仅增加files->count计数。这一共享使得任何线程都能访问进程所维护的打开文件,对它们的操作会直接反映到进程中的其他线程。

    4.CLONE_SIGHAND

    每一个Linux进程都可以自行定义对信号的处理方式,在task_struct中的sig(struct signal_struct)中使用一个struct k_sigaction结构的数组来保存这个配置信息,do_fork()中的copy_sighand()负责复制该信息;轻量级进程不进行复制,而仅仅增加signal_struct::count计数,与父进程共享该结构。也就是说,子进程与父进程的信号处理方式完全相同,而且可以相互更改。

    do_fork()中所做的工作很多,在此不详细描述。对于SMP系统,所有的进程fork出来后,都被分配到与父进程相同的cpu上,一直到该进程被调度时才会进行cpu选择。

    尽管Linux支持轻量级进程,但并不能说它就支持核心级线程,因为Linux的"线程"和"进程"实际上处于一个调度层次,共享一个进程标识符空间,这种限制使得不可能在Linux上实现完全意义上的POSIX线程机制,因此众多的Linux线程库实现尝试都只能尽可能实现POSIX的绝大部分语义,并在功能上尽可能逼近。

    三.LinuxThread的线程机制

    LinuxThreads是目前Linux平台上使用最为广泛的线程库,由Xavier Leroy (Xavier.Leroy@inria.fr)负责开发完成,并已绑定在GLIBC中发行。它所实现的就是基于核心轻量级进程的"一对一"线程模型,一个线程实体对应一个核心轻量级进程,而线程之间的管理在核外函数库中实现。

    1.线程描述数据结构及实现限制

    LinuxThreads定义了一个struct _pthread_descr_struct数据结构来描述线程,并使用全局数组变量__pthread_handles来描述和引用进程所辖线程。在__pthread_handles中的前两项,LinuxThreads定义了两个全局的系统线程:__pthread_initial_thread和__pthread_manager_thread,并用__pthread_main_thread表征__pthread_manager_thread的父线程(初始为__pthread_initial_thread)。

    struct _pthread_descr_struct是一个双环链表结构,__pthread_manager_thread所在的链表仅包括它一个元素,实际上,__pthread_manager_thread是一个特殊线程,LinuxThreads仅使用了其中的errno、p_pid、p_priority等三个域。而__pthread_main_thread所在的链则将进程中所有用户线程串在了一起。经过一系列pthread_create()之后形成的__pthread_handles数组将如下图所示:


    图2 __pthread_handles数组结构  

    新创建的线程将首先在__pthread_handles数组中占据一项,然后通过数据结构中的链指针连入以__pthread_main_thread为首指针的链表中。这个链表的使用在介绍线程的创建和释放的时候将提到。

    LinuxThreads遵循POSIX1003.1c标准,其中对线程库的实现进行了一些范围限制,比如进程最大线程数,线程私有数据区大小等等。在LinuxThreads的实现中,基本遵循这些限制,但也进行了一定的改动,改动的趋势是放松或者说扩大这些限制,使编程更加方便。这些限定宏主要集中在sysdeps/unix/sysv/linux/bits/local_lim.h(不同平台使用的文件位置不同)中,包括如下几个:

    每进程的私有数据key数,POSIX定义_POSIX_THREAD_KEYS_MAX为128,LinuxThreads使用PTHREAD_KEYS_MAX,1024;私有数据释放时允许执行的操作数,LinuxThreads与POSIX一致,定义PTHREAD_DESTRUCTOR_ITERATIONS为4;每进程的线程数,POSIX定义为64,LinuxThreads增大到1024(PTHREAD_THREADS_MAX);线程运行栈最小空间大小,POSIX未指定,LinuxThreads使用PTHREAD_STACK_MIN,16384(字节)。

    2.管理线程

    "一对一"模型的好处之一是线程的调度由核心完成了,而其他诸如线程取消、线程间的同步等工作,都是在核外线程库中完成的。在LinuxThreads中,专门为每一个进程构造了一个管理线程,负责处理线程相关的管理工作。当进程第一次调用pthread_create()创建一个线程的时候就会创建(__clone())并启动管理线程。

    在一个进程空间内,管理线程与其他线程之间通过一对"管理管道(manager_pipe[2])"来通讯,该管道在创建管理线程之前创建,在成功启动了管理线程之后,管理管道的读端和写端分别赋给两个全局变量__pthread_manager_reader和__pthread_manager_request,之后,每个用户线程都通过__pthread_manager_request向管理线程发请求,但管理线程本身并没有直接使用__pthread_manager_reader,管道的读端(manager_pipe[0])是作为__clone()的参数之一传给管理线程的,管理线程的工作主要就是监听管道读端,并对从中取出的请求作出反应。

    创建管理线程的流程如下所示: 
    (全局变量pthread_manager_request初值为-1)


    图3 创建管理线程的流程  

    初始化结束后,在__pthread_manager_thread中记录了轻量级进程号以及核外分配和管理的线程id,2*PTHREAD_THREADS_MAX+1这个数值不会与任何常规用户线程id冲突。管理线程作为pthread_create()的调用者线程的子线程运行,而pthread_create()所创建的那个用户线程则是由管理线程来调用clone()创建,因此实际上是管理线程的子线程。(此处子线程的概念应该当作子进程来理解。)

    __pthread_manager()就是管理线程的主循环所在,在进行一系列初始化工作后,进入while(1)循环。在循环中,线程以2秒为timeout查询(__poll())管理管道的读端。在处理请求前,检查其父线程(也就是创建manager的主线程)是否已退出,如果已退出就退出整个进程。如果有退出的子线程需要清理,则调用pthread_reap_children()清理。

    然后才是读取管道中的请求,根据请求类型执行相应操作(switch-case)。具体的请求处理,源码中比较清楚,这里就不赘述了。

    3.线程栈

    在LinuxThreads中,管理线程的栈和用户线程的栈是分离的,管理线程在进程堆中通过malloc()分配一个THREAD_MANAGER_STACK_SIZE字节的区域作为自己的运行栈。

    用户线程的栈分配办法随着体系结构的不同而不同,主要根据两个宏定义来区分,一个是NEED_SEPARATE_REGISTER_STACK,这个属性仅在IA64平台上使用;另一个是FLOATING_STACK宏,在i386等少数平台上使用,此时用户线程栈由系统决定具体位置并提供保护。与此同时,用户还可以通过线程属性结构来指定使用用户自定义的栈。因篇幅所限,这里只能分析i386平台所使用的两种栈组织方式:FLOATING_STACK方式和用户自定义方式。

    在FLOATING_STACK方式下,LinuxThreads利用mmap()从内核空间中分配8MB空间(i386系统缺省的最大栈空间大小,如果有运行限制(rlimit),则按照运行限制设置),使用mprotect()设置其中第一页为非访问区。该8M空间的功能分配如下图:


    图4 栈结构示意  

    低地址被保护的页面用来监测栈溢出。

    对于用户指定的栈,在按照指针对界后,设置线程栈顶,并计算出栈底,不做保护,正确性由用户自己保证。

    不论哪种组织方式,线程描述结构总是位于栈顶紧邻堆栈的位置。

    4.线程id和进程id

    每个LinuxThreads线程都同时具有线程id和进程id,其中进程id就是内核所维护的进程号,而线程id则由LinuxThreads分配和维护。

    __pthread_initial_thread的线程id为PTHREAD_THREADS_MAX,__pthread_manager_thread的是2*PTHREAD_THREADS_MAX+1,第一个用户线程的线程id为PTHREAD_THREADS_MAX+2,此后第n个用户线程的线程id遵循以下公式:

    	tid=n*PTHREAD_THREADS_MAX+n+1
    	

    这种分配方式保证了进程中所有的线程(包括已经退出)都不会有相同的线程id,而线程id的类型pthread_t定义为无符号长整型(unsigned long int),也保证了有理由的运行时间内线程id不会重复。

    从线程id查找线程数据结构是在pthread_handle()函数中完成的,实际上只是将线程号按PTHREAD_THREADS_MAX取模,得到的就是该线程在__pthread_handles中的索引。

    5.线程的创建

    在pthread_create()向管理线程发送REQ_CREATE请求之后,管理线程即调用pthread_handle_create()创建新线程。分配栈、设置thread属性后,以pthread_start_thread()为函数入口调用__clone()创建并启动新线程。pthread_start_thread()读取自身的进程id号存入线程描述结构中,并根据其中记录的调度方法配置调度。一切准备就绪后,再调用真正的线程执行函数,并在此函数返回后调用pthread_exit()清理现场。

    6.LinuxThreads的不足

    由于Linux内核的限制以及实现难度等等原因,LinuxThreads并不是完全POSIX兼容的,在它的发行README中有说明。

    1)进程id问题

    这个不足是最关键的不足,引起的原因牵涉到LinuxThreads的"一对一"模型。

    Linux内核并不支持真正意义上的线程,LinuxThreads是用与普通进程具有同样内核调度视图的轻量级进程来实现线程支持的。这些轻量级进程拥有独立的进程id,在进程调度、信号处理、IO等方面享有与普通进程一样的能力。在源码阅读者看来,就是Linux内核的clone()没有实现对CLONE_PID参数的支持。

    在内核do_fork()中对CLONE_PID的处理是这样的:

              if (clone_flags & CLONE_PID) {
                    if (current->pid)
                            goto fork_out;
            }
            

    这段代码表明,目前的Linux内核仅在pid为0的时候认可CLONE_PID参数,实际上,仅在SMP初始化,手工创建进程的时候才会使用CLONE_PID参数。

    按照POSIX定义,同一进程的所有线程应该共享一个进程id和父进程id,这在目前的"一对一"模型下是无法实现的。

    2)信号处理问题

    由于异步信号是内核以进程为单位分发的,而LinuxThreads的每个线程对内核来说都是一个进程,且没有实现"线程组",因此,某些语义不符合POSIX标准,比如没有实现向进程中所有线程发送信号,README对此作了说明。

    如果核心不提供实时信号,LinuxThreads将使用SIGUSR1和SIGUSR2作为内部使用的restart和cancel信号,这样应用程序就不能使用这两个原本为用户保留的信号了。在Linux kernel 2.1.60以后的版本都支持扩展的实时信号(从_SIGRTMIN到_SIGRTMAX),因此不存在这个问题。

    某些信号的缺省动作难以在现行体系上实现,比如SIGSTOP和SIGCONT,LinuxThreads只能将一个线程挂起,而无法挂起整个进程。

    3)线程总数问题

    LinuxThreads将每个进程的线程最大数目定义为1024,但实际上这个数值还受到整个系统的总进程数限制,这又是由于线程其实是核心进程。

    在kernel 2.4.x中,采用一套全新的总进程数计算方法,使得总进程数基本上仅受限于物理内存的大小,计算公式在kernel/fork.c的fork_init()函数中:

    	max_threads = mempages / (THREAD_SIZE/PAGE_SIZE) / 8
    	

    在i386上,THREAD_SIZE=2*PAGE_SIZE,PAGE_SIZE=2^12(4KB),mempages=物理内存大小/PAGE_SIZE,对于256M的内存的机器,mempages=256*2^20/2^12=256*2^8,此时最大线程数为4096。

    但为了保证每个用户(除了root)的进程总数不至于占用一半以上物理内存,fork_init()中继续指定:

        init_task.rlim[RLIMIT_NPROC].rlim_cur = max_threads/2;
        init_task.rlim[RLIMIT_NPROC].rlim_max = max_threads/2;
        

    这些进程数目的检查都在do_fork()中进行,因此,对于LinuxThreads来说,线程总数同时受这三个因素的限制。

    4)管理线程问题

    管理线程容易成为瓶颈,这是这种结构的通病;同时,管理线程又负责用户线程的清理工作,因此,尽管管理线程已经屏蔽了大部分的信号,但一旦管理线程死亡,用户线程就不得不手工清理了,而且用户线程并不知道管理线程的状态,之后的线程创建等请求将无人处理。

    5)同步问题

    LinuxThreads中的线程同步很大程度上是建立在信号基础上的,这种通过内核复杂的信号处理机制的同步方式,效率一直是个问题。

    6)其他POSIX兼容性问题

    Linux中很多系统调用,按照语义都是与进程相关的,比如nice、setuid、setrlimit等,在目前的LinuxThreads中,这些调用都仅仅影响调用者线程。

    7)实时性问题

    线程的引入有一定的实时性考虑,但LinuxThreads暂时不支持,比如调度选项,目前还没有实现。不仅LinuxThreads如此,标准的Linux在实时性上考虑都很少。

    四.其他的线程实现机制

    LinuxThreads的问题,特别是兼容性上的问题,严重阻碍了Linux上的跨平台应用(如Apache)采用多线程设计,从而使得Linux上的线程应用一直保持在比较低的水平。在Linux社区中,已经有很多人在为改进线程性能而努力,其中既包括用户级线程库,也包括核心级和用户级配合改进的线程库。目前最为人看好的有两个项目,一个是RedHat公司牵头研发的NPTL(Native Posix Thread Library),另一个则是IBM投资开发的NGPT(Next Generation Posix Threading),二者都是围绕完全兼容POSIX 1003.1c,同时在核内和核外做工作以而实现多对多线程模型。这两种模型都在一定程度上弥补了LinuxThreads的缺点,且都是重起炉灶全新设计的。

    1.NPTL

    NPTL的设计目标归纳可归纳为以下几点:

    • POSIX兼容性
    • SMP结构的利用
    • 低启动开销
    • 低链接开销(即不使用线程的程序不应当受线程库的影响)
    • 与LinuxThreads应用的二进制兼容性
    • 软硬件的可扩展能力
    • 多体系结构支持
    • NUMA支持
    • 与C++集成

    在技术实现上,NPTL仍然采用1:1的线程模型,并配合glibc和最新的Linux Kernel2.5.x开发版在信号处理、线程同步、存储管理等多方面进行了优化。和LinuxThreads不同,NPTL没有使用管理线程,核心线程的管理直接放在核内进行,这也带了性能的优化。

    主要是因为核心的问题,NPTL仍然不是100%POSIX兼容的,但就性能而言相对LinuxThreads已经有很大程度上的改进了。

    2.NGPT

    IBM的开放源码项目NGPT在2003年1月10日推出了稳定的2.2.0版,但相关的文档工作还差很多。就目前所知,NGPT是基于GNU Pth(GNU Portable Threads)项目而实现的M:N模型,而GNU Pth是一个经典的用户级线程库实现。

    按照2003年3月NGPT官方网站上的通知,NGPT考虑到NPTL日益广泛地为人所接受,为避免不同的线程库版本引起的混乱,今后将不再进行进一步开发,而今进行支持性的维护工作。也就是说,NGPT已经放弃与NPTL竞争下一代Linux POSIX线程库标准。

    3.其他高效线程机制

    此处不能不提到Scheduler Activations。这个1991年在ACM上发表的多线程内核结构影响了很多多线程内核的设计,其中包括Mach3.0、NetBSD和商业版本Digital Unix(现在叫Compaq True64 Unix)。它的实质是在使用用户级线程调度的同时,尽可能地减少用户级对核心的系统调用请求,而后者往往是运行开销的重要来源。采用这种结构的线程机制,实际上是结合了用户级线程的灵活高效和核心级线程的实用性,因此,包括Linux、FreeBSD在内的多个开放源码操作系统设计社区都在进行相关研究,力图在本系统中实现Scheduler Activations。


    参考资料

    • [Linus Torvalds,2002] Linux内核源码v2.4.20

    • [GNU,2002] Glibc源码v2.2.2(内含LinuxThreads v0.9)

    • [Thomas E. Terrill,1997] An Introduction to Threads Using The LinuxThreads Interface

    • [Ulrich Drepper,Ingo Molnar,2003] The Native POSIX Thread Library for Linux

    • http://www.ibm.com/developerworks/oss/pthreads/,NGPT官方网站 

    • [Ralf S. Engelschall,2000] Portable Multithreading

    • [Thomas E. Anderson, Brian N. Bershad, Edward D. Lazowska, Henry M. Levy,1992] Scheduler Activations: Effective Kernel Support for the User-Level Management of Parallelism

    • [pcjockey@21cn.com] Linux线程初探
    展开全文
  • 实验二 Linux 进程线程及编程实验 一实验目的 1 通过编写一个完整的守护进程掌握守护进程编写和调试的方法 2 进一步熟悉如何编写多进程程序 二实验环境 硬件PC 机一台JXARM9 2410 教学实验平台 软件Windows98/XP/...
  • linux线程知识

    千次阅读 2012-01-08 21:54:26
    一、基本概念 1、线程是计算机中独立运行...而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需
  • linux学习之八---Linux进程基础知识

    千次阅读 2014-10-10 16:53:53
    一、linux进程 linux是一个多用户多任务的操作系统。 多用户是指多个用户可以在同一时间使用计算机; 多任务是指linux可以同时执行几个任务。 进程简单来说就是运行中的程序,Linux系统的一个重要特点是可以同时...
  • linux 线程编程知识

    2010-10-14 23:39:00
    字号:大中小线程编程知识索引:单线程复制模型安全复制pthread_atfork多线程复制模型线程环境的exec()和exit()非局部跳转语句setjmp()和longjmp()信号的扩展线程安全函数接口的多线程安全性级别对应于不安全接口的...
  • Linux下查看进程线程数的方法

    千次阅读 2020-05-21 22:47:54
    阅读文本大概需要3分钟。0x01:ps -ef只打印进程,而ps -eLf会打印所有的线程[root@centos6~]#ps-ef|greprsyslogd root...
  • Linux进程快照相关知识

    千次阅读 2016-04-27 14:01:14
    查寻内核版本 uname -a // uname -r ...进程快照 ...ps report a snapshot of the current processes ...PID 进程ID 进程号 %CPU 进程占用CPU百分比 %MEM 进程占用内存的百分比 VSZ 虚拟内存 RS
  • 说到进程,恐怕面试中最常见的问题就是线程进程的关系了,那么先说一下答案:在 Linux 系统中,进程线程几乎没有区别。Linux 中的进程其实就是一个数据结构,顺带可以理解文件描述符...
  • 本章主要介绍进程的概念、状态、构成以及Linux进程的相关知识。 掌握进程的概念 掌握进程的描述、状态及转换 理解进程的特征 了解Linux进程的描述及进程通信 掌握进程的同步与互斥,并能灵活运用 理解线程的概念及...
  • 1. 什么是进程线程 现代操作系统比如Mac OS X,UNIX,Linux,Windows等,都是支持“多任务”的操作系统。 1.1 百科解释 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和...
  • linux 线程 进程经典文章

    千次阅读 2009-03-04 14:36:00
    linux 线程 进程有关linux进程线程看过很多文章,我觉的这篇可以说最经典 ---------------------------------一.基础知识线程进程 按照教科书上的定义,进程是资源管理的...
  • linux进程线程

    2012-09-08 14:43:23
    有关linux进程线程看过很多文章,我觉的这篇可以说最经典 什么是进程 直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体, 这个内存体有自己的地址空间,有自己的堆,上级...
  • 首先,简要了解一下进程线程。对于操作系统而言,进程是核心之核心,整个现代操作系统的根本,就是以进程为单位在执行任务。系统的管理架构也是基于进程层面的。在按下电源键之后,计算机就开始了复杂的启动过程,...
  • 进程:是个动态的概念,指的是一个静态的程序对某个数据集的一次运行活动,而程序是静态的概念,是由代码和数据组成的程序块而已。 进程5大特点:动态性,并发性,独立运行性,异步性,和结构化的特性。 在多道程序...
  • Linux系统编程——线程基础知识

    千次阅读 2017-03-30 19:15:08
    轻量级的进程,本质仍是进程(在Linux环境下) 进程:独立地址空间,拥有PCB 线程:也有PCB,但没有独立的地址空间(共享) 区别:在于是否共享地址空间。 独居(进程);合租(线程)。 Linux下: 线程:最小的执行单位  ...
  • 本文主要对Linux下的多线程进行一个入门的介绍,虽然是入门,但是十分详细,希望大家通过本文所述,对Linux线程编程的概念有一定的了解。具体如下。 1 线程基本知识 进程是资源管理的基本单元,而线程是系统调度的...
  • Linux查看进程的所有子进程线程

    千次阅读 2018-11-15 15:59:09
    Linux查看进程的所有子进程线程
  • Android进程线程基本知识

    千次阅读 2013-11-19 00:31:35
    本文介绍Android平台中进程与线程的基本知识。    很早的时候就想介绍一下Android中的进程和线程,但... 当一个程序第一次启动的时候,Android会启动一个LINUX进程和一个主线程。默认的情况下,所有该程序的组件
  •  二 线程进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),一个线程可以创建和...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 67,685
精华内容 27,074
关键字:

linux进程线程的知识

linux 订阅