下面享有资源的基本单位,就创建的又是两个子进程了

2020-04-04 作者:美高梅-运维   |   浏览(182)

原来刚刚开始做Linux下面的多进程编程的时候,对于下面这段代码感到很奇怪:

lienhua34
2014-10-05

    在Linux下面,创建进程是一件十分有意思的事情。我们都知道,进程是操作系统下面享有资源的基本单位。那么,在linux下面应该怎么创建进程呢?其实非常简单,一个fork函数就可以搞定了。但是,我们需要清楚的是子进程与父进程之间除了代码是共享的之外,堆栈数据和全局数据均是独立的,主要是学习标准的H文件的包含。

线程

#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<stdarg.h>
#include<errno.h>
#define LEN 2
void err_exit(char *fmt,...);
int main(int argc,char *argv[])
{
    pid_t pid;
    int loop; 

    for(loop=0;loop<LEN;loop++)
    {
    if((pid=fork()) < 0)
        err_exit("[fork:%d]: ",loop);
    else if(pid == 0)
    {
       printf("Child processn"); 
    }
    else
    {
        sleep(5);
    }
    }

    return 0;
}

1 进程控制三部曲概述

UNIX 系统提供了 fork、exec、exit 和 wait 等基本的进程控制原语。通过这些进程控制原语,我们即可完成对进程创建、执行和终止等基本操作。进程的控制可以划分为三部曲,

• 第一部:fork 创建新进程。

• 第二部:exec 执行新程序。

• 第三部:exit 和 wait 处理终止和等待终止。

 linux下的C语言开发(创建进程)

引言&动机

为什么这段程序会创建3个子进程,而不是两个,为什么在第20行后面加上一个return 0;就创建的又是两个子进程了?原来一直搞不明白,后来了解了C语言程序的存储空间布局以及在fork之后父子进程是共享正文段(代码段CS)之后才明白这其中的缘由!具体原理是啥,且容我慢慢道来!

2 第一部:fork 创建新进程

在一个现有的进程中,我们可以通过调用 fork 函数来创建一个新进程,

#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程中返回子进程ID,出错则返回-1

由 fork 创建的新进程被称为子进程。fork 函数调用一次,但返回两次。两次返回的唯一区别是:子进程返回值为 0,而父进程的返回值是新子进程的进程 ID。因为 UNIX 系统没有提供一个函数以获取某个进程的所有子进程 ID,所以父进程可以通过 fork 函数的返回值获取其子进程的进程 ID,并进行后续的处理。而在子进程中,可以通过调用 getppid 函数获取其父进程的进程 ID。

fork 函数返回之后,子进程和父进程都各自继续执行 fork 调用之后的指令。子进程是父进程的副本。例如,子进程获得了父进程数据空间、堆和栈的副本。但是,父子进程共享正文段。

例子:

下面程序调用 fork 创建一个新进程,在父子进程中都分别打印当前进程 ID 和父进程 ID。

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int
main(void)
{
    pid_t pid;
    printf("before forkn");
    if ((pid = fork()) < 0) {
        printf("fork error: %sn", strerror(errno));
        exit(-1);
     } else if (pid == 0) {
        printf("in child process, process ID: %d, parent process ID: %dn", getpid(),  getppid());
    } else {
        printf("in parent process, process ID: %d, child process ID: %dn", getpid(), pid);
        sleep(1);
    }
    exit(0);
}

编译该程序,生成 forkdemo 文件,然后执行该文件,

lienhua34:demo$ gcc -o forkdemo forkdemo.c
lienhua34:demo$ ./forkdemo
before fork
in parent process, process ID: 3499, child process ID: 3500
in child process, process ID: 3500, parent process ID: 3499

[cpp] view plain copy

考虑一下这个场景,我们有10000条数据需要处理,处理每条数据需要花费1秒,但读取数据只需要0.1秒,每条数据互不干扰。该如何执行才能花费时间最短呢?

首先得明白一个东西就是C程序的存储空间布局,如下图所示:

第二部:exec 执行新程序

用 fork 函数创建子进程后,子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程执行的程序完全替换为新程序,而新程序则从其 main 函数开始执行。调用 exec 并没有创建新进程,所以进程 ID 没有改变,exec 只是用一个新的程序替换了当前进程的正文、数据、堆和栈段。

UNIX 提供了 6 种不同的 exec 函数可供使用。我们在这里只说明其中的一种,

#include <unistd.h>
int execv(const char *pathname, char *const argv[]);
返回值:若出错则返回-1,若成功则没有返回值

其中 pathname 是进程要执行的新程序文件的路径,而 argv 参数则是要传递给新程序的参数列表。

例子:

我们有一个 sayhello.c 的程序文件,其代码如下,

#include <stdio.h>
#include <stdlib.h>
int
main(void)
{
    printf("Hello World! process ID: %dn", getpid());
    exit(0);
}

编译 sayhello.c 程序,生成执行文件 sayhello,

lienhua34:demo$ gcc -o sayhello sayhello.c
lienhua34:demo$ pwd
/home/lienhua34/program/c/apue/ch08/demo
lienhua34:demo$ ./sayhello
Hello World! process ID: 3545

在 execdemo.c 程序文件中,我们通过 fork 创建新进程,然后在子进程中调用 execv 函数执行 sayhello 文件,其代码如下,

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
char sayhelloPath[] = "/home/lienhua34/program/c/apue/ch08/demo/sayhello";
int
main(void)
{
    pid_t pid;
    if ((pid = fork()) < 0) {
        printf("fork error: %sn", strerror(errno));
        exit(-1);
    } else if (pid == 0) {
        printf("in child process, process ID: %d, parent process ID: %dn", getpid(), getppid());
        if (execv(sayhelloPath, NULL) < 0) {
            printf("execv error: %sn", strerror(errno));
            exit(-1);
        }
    } else {
        printf("in parent process, process ID: %d, child process ID: %dn", getpid(), pid);
        sleep(2);
    }
    exit(0);
}

编译 execdemo.c 文件,生成并执行 execdemo 文件,

lienhua34:demo$ gcc -o execdemo execdemo.c
lienhua34:demo$ ./execdemo
in parent process, process ID: 3561, child process ID: 3562
in child process, process ID: 3562, parent process ID: 3561
Hello World! process ID: 3562

从上面的运行结果可以看出,子进程(ID:3562)正常执行 sayhello 文件。

 

在多线程(MT)编程出现之前,电脑程序的运行由一个执行序列组成,执行序列按顺序在主机的中央处理器(CPU)中运行。无论是任务本身要求顺序执行还是整个程序是由多个子任务组成,程序都是按这种方式执行的。即使子任务相互独立,互相无关(即,一个子任务的结果不影响其它子 任务的结果)时也是这样。

图片 1
(原图出自《UNIX环境高级编程》7.6节)

4 第三部:exit 和 wait 处理终止和等待终止

exit 函数提供了进程终止的功能,在“进程终止”一文中,我们已经学习了 exit 函数的基本知识。我们这里补充一下关于进程的终止状态。在学习 exit 函数时,我们已经知道 exit 函数的参数作为程序的退出状态(exit status)。在进程终止时,内核会将程序的退出状态作为进程的终止状态(termination status)。在进程异常终止的时候,内核会产生一个指示其异常终止原因的终止状态。无论进程是正常终止还是异常终止,该终止进程的父进程都可以通过 wait 或 waitpid 函数获取其终止状态。

在前面两节中的 forkdemo.c 和 execdemo.c 程序中,fork 调用之后的父进程都调用了 sleep 函数休眠一下。这是因为,在调用 fork 函数之后是父进程先执行还是子进程先执行是不确定的。于是,我们在父进程中调用sleep 休眠一段时间,让子进程先执行结束(注:这样子也不能确保)。

我们可以在父进程中调用 wait 来等待子进程结束,

#include <sys/wait.h>
pid_t wait(int *statloc);
返回值:若成功则返回进程ID,若出错则返回-1

statloc 参数是一个整型指针。如果 statloc 不是一个空指针,则终止进程的终止状态就存储在该指针指向的内存单元中。如果不关心终止状态,则可以将参数设置为 NULL。

进程调用 wait 会导致该调用者阻塞,直到某个子进程终止。如果调用wait 函数时,调用进程没有任何子进程则立即出错返回。

例子:

下面程序在上一节的 execdemo.c 基础上,将 fork 之后父进程的 sleep语句替换成 wait 来等待子进程的结束。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>
char sayhelloPath[] = "/home/lienhua34/program/c/apue/ch08/demo/sayhello";
int
main(void)
{
    pid_t pid;
    int waitres;
    if ((pid = fork()) < 0) {
        printf("fork error: %sn", strerror(errno));
        exit(-1);
    } else if (pid == 0) {
        printf("in child process, process ID: %d, parent process ID: %dn", getpid(), getppid());
        if (execv(sayhelloPath, NULL) < 0) {
            printf("execv error: %sn", strerror(errno));
            exit(-1);
       }
    } else {
        printf("in parent process, process ID: %d, child process ID: %dn", getpid(), pid);
        if ((waitres = wait(NULL)) < 0) {
            printf("wait error: %sn", strerror(errno));
            exit(-1);
        } else {
            printf("termination child process: %dn", waitres);
        }
    }
    exit(0);
}

编译该程序,生成并执行 execdemo 文件,

lienhua34:demo$ gcc -o execdemo execdemo.c
lienhua34:demo$ ./execdemo
in parent process, process ID: 3795, child process ID: 3796
in child process, process ID: 3796, parent process ID: 3795
Hello World! process ID: 3796
termination child process: 3796

(done)

  1. #include <unistd.h>  
  2. #include <stdio.h>  
  3. #include <stdlib.h>  
  4. #include <math.h>  
  5. #include <errno.h>  
  6. #include <sys/types.h>  
  7. #include <sys/wait.h>  
  8.   
  9. int main()  
  10. {  
  11.     pid_t pid;  
  12.   
  13.     if(-1 == (pid = fork()))  
  14.     {  
  15.         printf("Error happened in fork function!n");  
  16.         return 0;  
  17.     }  
  18.   
  19.     if(0 == pid)  
  20.     {  
  21.         printf("This is child process: %dn", getpid());  
  22.     }  
  23.     else  
  24.     {  
  25.         printf("This is parent process: %dn", getpid());  
  26.     }  
  27.   
  28.     return 0;  
  29. }  
  30.      

对于上边的问题,如果使用一个执行序列来完成,我们大约需要花费10000*0.1 + 10000 = 11000 秒。这个时间显然是太长了。

当一个C程序执行之后,它会被加载到内存之中,它在内存中的布局如上图,分为这么几个部分,环境变量和命令行参数、栈、堆、数据段(初始化和未初始化的)、正文段,下面挨个来说明这几段分别代表了什么:

==================================================================================

那我们有没有可能在执行计算的同时取数据呢?或者是同时处理几条数据呢?如果可以,这样就能大幅提高任务的效率。这就是多线程编程的目的。

环境变量和命令行参数:这些指的就是Unix系统上的环境变量(比如$PATH)和传给main函数的参数(argv指针所指向的内容)。

linux下的C语言开发(多线程编程)

对于本质上就是异步的,需要有多个并发事务,各个事务的运行顺序可以是不确定的,随机的,不可预测的问题,多线程是最理想的解决方案。这样的任务可以被分成多个执行流,每个流都有一个要完成的目标,然后将得到的结果合并,得到最终的结果。

数据段:这个是指在C程序中定义的全局变量,如果没有初始化,那么就存放在未初始化的数据段中,程序运行时统一由exec赋值为0。否则就存放在初始化的数据段中,程序运行时由exec统一从程序文件中读取。(了解汇编的朋友们想必知道汇编语言中的数据段DS,这和汇编中的数据段其实是一个东西)。

    多线程和多进程还是有很多区别的。其中之一就是,多进程是Linux内核本身所支持的,而多线程则需要相应的动态库进行支持。对于进程而言,数据之间都是相互隔离的,而多线程则不同,不同的线程除了堆栈空间之外所有的数据都是共享的。说了这么多,我们还是自己编写一个多线程程序看看结果究竟是怎么样的。

线程和进程

堆:这一部分主要用来动态分配空间。比如在C语言中用malloc申请的空间就是在这个区域申请的。

 

什么是进程

正文段:C语言代码并不是直接执行的,而是被编译成了机器指令才能够在电脑上执行,最终生成的机器指令就是存放在这个区域(汇编中的代码段CS指的就是这片区域)。

[cpp] view plain copy

进程(有时被称为重量级进程)是程序的一次 执行。每个进程都有自己的地址空间,内存,数据栈以及其它记录其运行轨迹的辅助数据。操作系 统管理在其上运行的所有进程,并为这些进程公平地分配时间。进程也可以通过 fork 和 spawn 操作 来完成其它的任务。不过各个进程有自己的内存空间,数据栈等,所以只能使用进程间通讯(IPC), 而不能直接共享信息。

栈:个人感觉这是C程序内存布局最关键的部分了。这个部分主要用来做函数调用。具体而言怎么说呢,程序刚开始栈中只有main这一个函数的内容(即main的栈帧),如果main函数要调用func函数,那么func函数的返回地址(main函数的地址),func函数的参数,func函数中定义的局部变量,还有func函数的返回值等等这些都会被压入栈中,这时栈中就多了func函数的内容(func的栈帧)。然后func函数运行完了之后再来弹栈,把它原来压的内容去掉(即清除掉func栈帧),此时栈中又只剩下了main的栈帧。(这片区域就是汇编中的栈段SS)

 

什么是线程

OK,这就是C程序的存储器布局。这里我联想到另外一点,就是全局变量和静态变量是存储在数据段中的,而局部变量是存储在栈中的,栈中数据在函数调用完之后一弹栈就没了,这就是为什么全局变量的生存周期比局部变量的生存周期要长的原因。

  1. #include <stdio.h>  
  2. #include <pthread.h>  
  3. #include <unistd.h>  
  4. #include <stdlib.h>  
  5.   
  6. void func_1(void* args)  
  7. {  
  8.     while(1){  
  9.         sleep(1);  
  10.         printf("this is func_1!n");  
  11.     }  
  12. }  
  13.   
  14. void func_2(void* args)  
  15. {  
  16.     while(1){  
  17.         sleep(2);  
  18.         printf("this is func_2!n");  
  19.     }  
  20. }  
  21.   
  22. int main()  
  23. {  
  24.     pthread_t pid1, pid2;  
  25.   
  26.     if(pthread_create(&pid1, NULL, func_1, NULL))  
  27.     {  
  28.         return -1;  
  29.     }  
  30.   
  31.     if(pthread_create(&pid2, NULL, func_2, NULL))  
  32.     {  
  33.         return -1;  
  34.     }  
  35.   
  36.     while(1){  
  37.         sleep(3);  
  38.     }  
  39.   
  40.     return 0;  
  41. }  

线程(有时被称为轻量级进程)跟进程有些相似,不同的是,所有的线程运行在同一个进程中, 共享相同的运行环境。它们可以想像成是在主进程或“主线程”中并行运行的“迷你进程”。

了解了C程序在存储器的布局之后,我们再来了解fork的内存复制机制,关于这个,我们只需要了解一句话就够了,“子进程复制父进程的数据空间(数据段)、栈和堆,父、子进程共享正文段。”也就是说,对于程序中的数据,子进程要复制一份,但是对于指令,子进程并不复制而是和父进程共享。具体来看下面这段代码(这是我在上面那段代码上稍微添加了一点东西):

 

线程有开始,顺序执行和结束三部分。它有一个自己的指令指针,记录自己运行到什么地方。线程的运行可能被抢占(中断),或暂时的被挂起(也叫睡眠),让其它的线程运行,这叫做让步。 一个进程中的各个线程之间共享同一片数据空间,所以线程之间可以比进程之间更方便地共享数据以及相互通讯。

/*  这个程序会创建3个子进程,理解这句话,父子进程复制数据段、栈、堆,共享正文段
 *
 */
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<stdarg.h>
#include<errno.h>
#define BUFSIZE 512
#define LEN 2
void err_exit(char *fmt,...);
int main(int argc,char *argv[])
{
    pid_t pid;
    int loop; 

    for(loop=0;loop<LEN;loop++)
    {
    printf("Now is No.%d loop:n",loop);

    if((pid=fork()) < 0)
        err_exit("[fork:%d]: ",loop);
    else if(pid == 0)
    {
       printf("[Child process]P:%d C:%dn",getpid(),getppid()); 
    }
    else
    {
        sleep(5);
    }
    }

    return 0;
}

    和我们以前编写的程序有所不同,多线程代码需要这样编译,输入gcc thread.c -o thread -lpthread,编译之后你就可以看到thread可执行文件,输入./thread即可。

当然,这样的共享并不是完全没有危险的。如果多个线程共同访问同一片数据,则由于数据访问的顺序不一样,有可能导致数据结果的不一致的问题。这叫做竞态条件(race condition)。

为什么上面那段代码会创建三个子进程?我们来具体分析一下它的执行过程:

 

线程一般都是并发执行的,不过在单CPU 的系统中,真正的并发是不可能的,每个线程会被安排成每次只运行一小会,然后就把 CPU 让出来,让其它的线程去运行。由于有的函数会在完成之前阻塞住,在没有特别为多线程做修改的情 况下,这种“贪婪”的函数会让 CPU 的时间分配有所倾斜。导致各个线程分配到的运行时间可能不 尽相同,不尽公平。

首先父进程执行循环,通过fork创建一个子进程,然后sleep5秒。

[cpp] view plain copy

Python、线程和全局解释器锁

再来看父进程创建的这个子进程,这里我们记为子进程1.子进程1完全复制了这个父进程的数据部分,但是需要注意的是它的正文段是和父进程共享的。也就是说,子进程1开始执行代码的部分并不是从main的 { 开始执行的,而是主函数执行到哪里了,它就接着执行,具体而言就是它会执行fork后面的代码。所以子进程1首先会打印出它的ID和它的父进程的ID。然后继续第二遍循环,然后这个子进程1再来创建一个子进程,我们记为子进程11,子进程1开始sleep。

 

全局解释器锁(GIL)

子进程11接着子进程1执行的代码开始执行(即fork后面),它也是打印出它的ID和父进程ID(子进程1),然后此时loop的值再加1就等于2了,所以子进程2直接就返回了。

  1. [test@localhost Desktop]$ ./thread  
  2. this is func_1!  
  3. this is func_2!  
  4. this is func_1!  
  5. this is func_1!  
  6. this is func_2!  
  7. this is func_1!  
  8. this is func_1!  
  9. this is func_2!  
  10. this is func_1!  
  11. this is func_1!  

首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行(其中的JPython就没有GIL)。

那个子进程1sleep完了之后也是loop的值加1之后变成了2,所以子进程1也返回了!

 =================================================================

那么CPython实现中的GIL又是什么呢?GIL全称Global Interpreter Lock为了避免误导,我们还是来看一下官方给出的解释:

然后我们再返回去看父进程,它仅仅循环了一次,sleep完之后再来进行第二次循环,这次又创建了一个子进程我们记为子进程2。然后父进程开始sleep,sleep完了之后也结束了。

linux下的C语言开发(管道通信)

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

那么那个子进程2怎么样了呢?它从fork后开始执行,此时loop等于1,它打印完它的ID和父进程ID之后,就结束循环了,整个子进程2就直接结束了!

    Linux系统本身为进程间通信提供了很多的方式,比如说管道、共享内存、socket通信等。管道的使用十分简单,在创建了匿名管道之后,我们只需要从一个管道发送数据,再从另外一个管道接受数据即可。

尽管Python完全支持多线程编程, 但是解释器的C语言实现部分在完全并行执行时并不是线程安全的。 实际上,解释器被一个全局解释器锁保护着,它确保任何时候都只有一个Python线程执行。

这就是上面那段代码的运行流程,进程间的关系如下图所示:

 

在多线程环境中,Python 虚拟机按以下方式执行:

图片 2

[cpp] view plain copy

设置GIL

上图中那个loop=%d就是当这个进程开始执行的时候loop的值。上面那段代码的运行结果如下图:

 

切换到一个线程去执行

图片 3

  1. #include <stdio.h>  
  2. #include <unistd.h>  
  3. #include <stdlib.h>  
  4. #include <string.h>  
  5.   
  6. int pipe_default[2];    
  7.   
  8. int main()  
  9. {  
  10.     pid_t pid;  
  11.     char buffer[32];  
  12.   
  13.     memset(buffer, 0, 32);  
  14.     if(pipe(pipe_default) < 0)  
  15.     {  
  16.         printf("Failed to create pipe!n");  
  17.         return 0;  
  18.     }  
  19.   
  20.     if(0 == (pid = fork()))  
  21.     {  
  22.         close(pipe_default[1]);  
  23.         sleep(5);  
  24.         if(read(pipe_default[0], buffer, 32) > 0)  
  25.         {  
  26.             printf("Receive data from server, %s!n", buffer);  
  27.         }  
  28.         close(pipe_default[0]);  
  29.     }  
  30.     else  
  31.     {  
  32.         close(pipe_default[0]);  
  33.         if(-1 != write(pipe_default[1], "hello", strlen("hello")))  
  34.         {  
  35.             printf("Send data to client, hello!n");  
  36.         }  
  37.         close(pipe_default[1]);  
  38.         waitpid(pid, NULL, 0);  
  39.     }  
  40.   
  41.     return 1;  
  42. }  

运行

这里这个3498进程就是我们的主进程,3499就是子进程1,3500就是子进程11,3501就是子进程2。

    下面我们就可以开始编译运行了,老规矩分成两步骤进行:(1)输入gcc pipe.c -o pipe;(2)然后输入./pipe,过一会儿你就可以看到下面的打印了。

指定数量的字节码指令

最后,我们再来回答一下我们开始的时候提出的那个问题,为什么在子进程的处理部分“ if(pid == 0) ”最后加一个return 0,就会创建两个子进程了,就是因为子进程1运行到这里直接就结束了,不再进行第二遍循环了,所以就不会再去创建那个子进程11了,所以最后一共就是创建了两个子进程啊!

[cpp] view plain copy

线程主动让出控制(可以调用time.sleep(0))

 

把线程设置完睡眠状态

  1. [test@localhost pipe]$ ./pipe  
  2. Send data to client, hello!  
  3. Receive data from server, hello!  

解锁GIL

 =====================================================================

再次重复以上步骤

 linux下的C语言开发(信号处理)

对所有面向I/O 的(会调用内建的操作系统 C 代码的)程序来说,GIL 会在这个 I/O 调用之 前被释放,以允许其它的线程在这个线程等待 I/O 的时候运行。如果某线程并未使用很多 I/O 操作, 它会在自己的时间片内一直占用处理器(和 GIL)。也就是说,I/O 密集型的 Python 程序比计算密集 型的程序更能充分利用多线程环境的好处。

 

退出线程

    信号处理是Linux程序的一个特色。用信号处理来模拟操作系统的中断功能,对于我们这些系统程序员来说是最好的一个选择了。要想使用信号处理功能,你要做的就是填写一个信号处理函数即可。一旦进程有待处理的信号处理,那么进程就会立即进行处理。

当一个线程结束计算,它就退出了。线程可以调用thread.exit()之类的退出函数,也可以使用 Python 退出进程的标准方法,如 sys.exit()或抛出一个 SystemExit 异常等。不过,你不可以直接 “杀掉”(“kill”)一个线程。

 

在Python 中使用线程

[cpp] view plain copy

在Win32 和 Linux, Solaris, MacOS, *BSD 等大多数类 Unix 系统上运行时,Python 支持多线程 编程。Python 使用 POSIX 兼容的线程,即 pthreads。

 

默认情况下,只要在解释器中

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <signal.h>  
  4.   
  5. int value = 0;  
  6.   
  7. void func(int sig)  
  8. {  
  9.     printf("I get a signal!n");  
  10.     value = 1;  
  11. }  
  12.   
  13. int main()  
  14. {  
  15.     signal(SIGINT, func);  
  16.   
  17.     while(0 == value)  
  18.         sleep(1);  
  19.   
  20.     return 0;  
  21. }  

>> import thread

 

如果没有报错,则说明线程可用。

    为了显示linux对signal的处理流程,我们需要进行两个步骤。第一,输入gcc sig.c -o sig, 然后输入./sig即可;第二则重启一个console窗口,输入ps -aux | grep sig, 在获取sig的pid之后然后输入kill -INT 2082, 我们即可得到如下的输出。

Python 的 threading 模块

 

Python 供了几个用于多线程编程的模块,包括 thread, threading 和 Queue 等。thread 和 threading 模块允许程序员创建和管理线程。thread 模块 供了基本的线程和锁的支持,而 threading 供了更高级别,功能更强的线程管理的功能。Queue 模块允许用户创建一个可以用于多个线程之间 共享数据的队列数据结构。

[cpp] view plain copy

核心示:避免使用 thread 模块

 

出于以下几点考虑,我们不建议您使用thread 模块。

  1. [root@localhost fork]#./sig  
  2. I get a signal!  
  3. [root@localhost fork]#  

更高级别的threading 模块更为先 进,对线程的支持更为完善,而且使用 thread 模块里的属性有可能会与 threading 出现冲突。其次, 低级别的 thread 模块的同步原语很少(实际上只有一个),而 threading 模块则有很多。

 

对于你的进程什么时候应该结束完全没有控制,当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作。我们之前说过,至少threading 模块能确保重要的子线程退出后进程才退出。

 ===========================================================

thread 模块

 linux下的C语言开发(进程等待)

除了产生线程外,thread 模块也提供了基本的同步数 据结构锁对象(lock object,也叫原语锁,简单锁,互斥锁,互斥量,二值信号量)。

    所谓进程等待,其实很简单。前面我们说过可以用fork创建子进程,那么这里我们就可以使用wait函数让父进程等待子进程运行结束后才开始运行。注意,为了证明父进程确实是等待子进程运行结束后才继续运行的,我们使用了sleep函数。但是,在Linux下面,sleep函数的参数是秒,而windows下面sleep的函数参数是毫秒。

thread 模块函数

 

start_new_thread(function, args, kwargs=None):产生一个新的线程,在新线程中用指定的参数和可选的 kwargs 来调用这个函数。

[cpp] view plain copy

allocate_lock():分配一个 LockType 类型的锁对象

 

exit():让线程退出

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <unistd.h>  
  4.   
  5. int main(int argc, char* argv[])  
  6. {  
  7.     pid_t pid;  
  8.   
  9.     pid = fork();  
  10.     if(0 == pid)  
  11.     {  
  12.         printf("This is child process, %dn", getpid());  
  13.         sleep(5);  
  14.     }  
  15.     else  
  16.     {  
  17.         wait(NULL);  
  18.         printf("This is parent process, %dn", getpid());  
  19.     }  
  20.   
  21.     return 1;  
  22. }  

acquire(wait=None):尝试获取锁对象

    下面,我们需要做的就是两步,首先输入gcc fork.c -o fork, 然后输入./fork,就会在console下面获得这样的结果。

locked():如果获取了锁对象返回 True,否则返回 False

 

release():释放锁

 

start_new_thread()要求一定要有前两个参数。所以,就算我们想要运行的函数不要参数,也要传一个空的元组。

[cpp] view plain copy

为什么要加上sleep(6)这一句呢? 因为,如果我们没有让主线程停下来,那主线程就会运行下一条语句,显示 “all done”,然后就关闭运行着 loop()和 loop1()的两个线程,退出了。

 

我们有没有更好的办法替换使用sleep() 这种不靠谱的同步方式呢?答案是使用锁,使用了锁,我们就可以在两个线程都退出之后马上退出。

  1. [root@localhost fork]# ./fork  
  2. This is child process, 2135  
  3. This is parent process, 2134  

import thread

 

from time importsleep,time

 

loops=[4,2]

 ===================================================

def loop(nloop,nsec,lock):

linux下的C语言开发(线程等待)

print('start loop %s at: %s'%(nloop,time()))

    和多进程一样,多线程也有自己的等待函数。这个等待函数就是pthread_join函数。那么这个函数有什么用呢?我们其实可以用它来等待线程运行结束。

sleep(nsec)

 

print('loop %s done at: %s'%(nloop,time()))

[cpp] view plain copy

# 每个线程都会被分配一个事先已经获得的锁,在 sleep()的时间到了之后就释放 相应的锁以通知主线程,这个线程已经结束了。

 

lock.release()

  1. #include <stdio.h>  
  2. #include <pthread.h>  
  3. #include <unistd.h>  
  4. #include <stdlib.h>  
  5.   
  6. void func(void* args)  
  7. {  
  8.     sleep(2);  
  9.     printf("this is func!n");  
  10. }  
  11.   
  12. int main()  
  13. {  
  14.     pthread_t pid;  
  15.   
  16.     if(pthread_create(&pid, NULL, func, NULL))  
  17.     {  
  18.         return -1;  
  19.     }  
  20.   
  21.     pthread_join(pid, NULL);  
  22.     printf("this is end of main!n");  
  23.   
  24.     return 0;  
  25. }  

def main():

 

print('starting at:',time())

    编写wait.c文件结束之后,我们就可以开始编译了。首先你需要输入gcc wait.c -o wait -lpthread,编译之后你就可以看到wait可执行文件,输入./wait即可。

locks=[]

 

nloops=range(len(loops))

[cpp] view plain copy

foriinnloops:

 

# 调用 thread.allocate_lock()函数创建一个锁的列表

  1. [test@localhost thread]$ ./thread  
  2. this is func!  
  3. this is end of main!  

lock=thread.allocate_lock()

 

# 分别调用各个锁的 acquire()函数获得, 获得锁表示“把锁锁上”

 

lock.acquire()

 =====================================================

locks.append(lock)

linux下的C语言开发(线程互斥)

foriinnloops:

本文由美高梅赌堵59599发布于美高梅-运维,转载请注明出处:下面享有资源的基本单位,就创建的又是两个子进程了

关键词: