Linux C进程与多线程

来源:互联网 发布:笔记本电脑周边 知乎 编辑:程序博客网 时间:2024/05/18 01:26

进程和程序的区别

进程和程序的区别可以理解为,进程是活动的程序,而程序是一个代码的集合。进程是加载到内存中的程序,而程序没有加载到内存中,之在磁盘上保存着。下图是进程的结构,而程序进包含代码段

 +-------------+  |  代码段      | +-------------+  |   堆栈段     | +-------------+ |   数据段     | +-------------+ 

代码实例
fork.c :

#include <sys/types.h>#include <unistd.h>#include <stdio.h>int main(){    pid_t pid;    char *message;    int n;    printf("fork program starting\n");    pid = fork();    switch(pid)     {    case -1:        perror("fork failed");        exit(1);    case 0:        message = "This is the child";        n = 5;        break;    default:        message = "This is the parent";        n = 3;        break;    }    for(; n > 0; n--) {        puts(message);        sleep(1);    }    exit(0);}

(1) pid_t是进程号,是唯一表示进程的ID。

(2) pid_t fork(void) 函数

包含的头文件:

#include <sys/types.h>#include <unistd.h>

调用fork可以创建一个全新的进程。这个系统调用对当前进程进行复制。在进程表里创建一个新的项目,新项目的许多属性与当前进程是相同的。新进程和原进程几乎一模一样,执行的也是相同的代码,但新进程有自己的数据空间、自己的环境等。

(3) 程序调用了fork函数的时候被分成了两个进程。在父进程里,fork函数返回新进程的PID进程号,新进程则返回0,这个可以做为区分父子进程的依据。
这里写图片描述

父进程和子进程的执行的代码都和fork.c里的代码一致。但是,fork根据不同进程返回不同的PID,那么父子进程的实际有效代码部分是不同的,下面只写实际有效的代码:
这里写图片描述

就是说,进程会根据PID的不同,有选择的执行各自的代码。

这个程序将产生两个进程,新进程(子进程)会输出消息5次,而父进程之输出3次。父进程会在子进程打印完它的全部消息之前退出。运行一下这个程序,我们可以看到如下交替输出的消息:
这里写图片描述

可以看到创建进程后,消息的输出是父子进程交替输出,且父进程在子进程之前结束。

wait()

如果要安排父进程在子进程结束之后才结束,则需要调用wait函数。

函数说明

pid_t wait(int * stat_loc)

包含的头文件:

#include <sys/types.h>#include <sys/wait.h>

返回值:子进程的PID

参数:如果stat_loc不是一个空指针,状态信息将被写入它指向的位置

sys/wait.h文件中的状态信息见下表:

宏定义       说明WIFEXITED(stat_val) 如果子进程正常结束,它就取一个非零值WEXITSTATUS(stat_val) 如果WIFEXITED非零,它返回子进程的退出码WIFSIGNALED(stat_val) 如果子进程因为一个未捕获的信号而终止,它就取一个非零值WTERMSIG(stat_val) 如果WIFSIGNALED非零,它返回一个信号代码WIFSTOPPED(stat_val) 如果子进程终止,它就取一个非零值WSTOPSIG(stat_val) 如果WIFSTOPPED非零,它返回一个信号代码

wait系统调用会使父进程暂停执行,直到它的一个子进程结束为止。

代码实例:

#include <sys/types.h>#include <sys/wait.h>#include <unistd.h>#include <stdio.h>int main(){ pid_t pid; char * message; int n; int exit_code; printf("fork program starting\n"); pid = fork(); switch(pid){  case -1:   perror("fork failed");   exit(1);  case 0:   message ="This is the child";   n = 5;   /*子进程的退出码*/   exit_code = 37;   break;  default:   message = "This is the parent";   n = 3;   /*父进程的退出码*/   exit_code = 0;   break;  }  /*pid非0,在父进程执行*/  if(pid){   int stat_val;   pid_t child_pid;   /*父进程直到子进程推出后执行*/   child_pid = wait(&stat_val);      printf("Child process has finished: PID=%d\n",child_pid);   if(WIFEXITED(stat_val))    /*子进程正常结束,输出子进程退出码,即exit_code=37*/    printf("Child exited with code %d\n", WEXITSTATUS(stat_val));   else    /*子进程非正常结束*/    printf("Child terminated abnormally\n");  }  for(; n > 0; n--){    puts(message);    sleep(1);    }  exit(exit_code);}

父进程通过wait系统调用把自己的执行挂起,直到子进程的状态信息出现为止。这将发生在子进程调用exit的时候;我们把它的退出码设置为37.

然后,父进程继续执行,通过测试wait调用的返回值确定子进程的已经正常结束,并从状态信息里提取出子进程的退出码。

运行效果见下图:
这里写图片描述

信号

信号是系统响应某些状况而产生的事件,进程在接受到信号时会采取相应的行动。信号可以明确地由一个进程产生发送到另外一个进程,用这种办法传递信息或协调操作行为。

进程可以生成信号、捕捉并相应信号或屏蔽信号。信号的名称是在头文件signal.h里定义。下面我列出一部分,如下:

信号名称    说明SIGALRM   警告钟SIGHUP    系统挂断SIGINT    终端中断SIGKILL   停止进程(此信号不能被忽略或捕获)SIGPIPE   向没有读者的管道写数据SIGQUIT   终端退出SIGTERM   终止

如果进程接收到上表中的某个信号但实现并没有安排捕捉它,进程就会立刻终止。
函数

#include <signal.h>void (*signal(int sig, void (*func) (int) ))(int);

第一个参数sig就是准备捕获或屏蔽的信号,接收到指定信号时将调用func函数处理。

实例1—处理SIGINT信号

#include <signal.h>#include <stdio.h>#include <unistd.h>void ouch(int sig){      /*此处,signal(SIGINT,  SIG_DFL),SIG_DFL表示 ouch函数捕获到SIGINT信号,作出输出信息处理之后,恢复了SIGINT的默认行为*/printf("OUCH! - I got signal %d\n", sig);(void) signal(SIGINT, SIG_DFL);}int main(){     (void) signal(SIGINT, ouch);     while(1) {      printf("Hello World!\n");       sleep(1);     } }

这个程序就是截获组合键Ctrl+C产生的SIGINT信号。没有信号出现时,它每隔一秒就会输出一个消息。第一次按下Ctrl+C产生的SIGINT信号,程序会调用ouch函数,输出信息,同时,恢复SIGINT为默认行为(即按下Ctrl+C组合键后即结束运行),那么第二次按下Ctrl+C组合键时,程序就结束了运行。
这里写图片描述

实例2—模仿闹钟行为
使用到的函数:
函数1

#include <sys/types.h>#include <signal.h>int kill (pid_t pid, int sig);

kill函数的作用是把sig信号发送给标识为pid的进程,成功时返回“0”,失败时返回“-1”. 要想发送一个信号,两个进程(发送和接受两方)必须拥有同样的用户ID,,就是说,
你只能想自己的另一个进程发送信号。但是超级用户可以向任何进程发送信号。

函数2

#include <unistd.h>unsigned int alarm (unsigned int seconds ) ;

alarm函数是在seconds秒后安排发送一个SIGALARM信号。若seconds为0,表示将取消全部已经设置的闹钟请求。每一个进程只有一个可用的闹钟。
它的返回值是前一个闹钟闹响之前还需经过的剩余秒数。调用失败则返回“-1”.

#include <signal.h>#include <stdio.h>#include <unistd.h>static int alarm_fired = 0;void ding(int sig){    alarm_fired = 1;}int main(){    int pid;    printf("闹钟程序已经启动\n");    /*子进程休眠5秒后向父进程发送SIGALARM信号,然后结束进程*/    if((pid = fork()) == 0) {        sleep(5);        kill(getppid(), SIGALRM);        exit(0);    }  /*父进程执行的内容*/    printf("5秒后闹铃启动\n");    (void) signal(SIGALRM, ding);    /*将运行的程序挂起,直到接收到信号为止*/    pause();    if (alarm_fired)        printf("Ding!\n");    printf("done\n");    exit(0);}

程序通过fork启动一个新进程,这个紫禁城休眠5秒后向 自己的父进程发送一个SIGALARM信号。父进程在安排好捕捉SIGALARM信号后暂停运行,直到接收到一个信号为止。
运行结果见下图:
这里写图片描述

进程与线程

(1) 线程是进程的一个实体,是CPU调度和分派的基本单位,,它是比进程更小的能独立运行的基本单位.

(2) 进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

下图是多线程的结构:
这里写图片描述

而进程之间的通信有两种方式,一种是在两个进程之间分配一个共享内存区域,另一种方法是通过内核来通信
这里写图片描述

_REENTRANT宏

在一个多线程程序里,默认情况下,只有一个errno变量供所有的线程共享。在一个线程准备获取刚才的错误代码时,该变量很容易被另一个线程中的函数调用所改变。类似的问题还存在于fputs之类的函数中,这些函数通常用一个单独的全局性区域来缓存输出数据。

为解决这个问题,需要使用可重入的例程。可重入代码可以被多次调用而仍然工作正常。编写的多线程程序,通过定义宏_REENTRANT来告诉编译器我们需要可重入功能,这个宏的定义必须出现于程序中的任何#include语句之前。

_REENTRANT为我们做三件事情:

(1)它会对部分函数重新定义它们的可安全重入的版本,这些函数名字一般不会发生改变,只是会在函数名后面添加_r字符串,如函数名gethostbyname变成gethostbyname_r。

(2)stdio.h中原来以宏的形式实现的一些函数将变成可安全重入函数。

(3)在error.h中定义的变量error现在将成为一个函数调用,它能够以一种安全的多线程方式来获取真正的errno的

基本函数

(1) pthread_create函数

#include <pthread.h>int pthread_create ( pthread_t  *thread,  pthread_attr_t  * attr,  void*  (*start_routine)(void*),  void *arg );

返回值:调用成功返回“0”,如果失败则返回一个错误。

第一个参数:进程创建时,会分配一个唯一的PID标识,同样的,线程创建时,也会用一个指向pthread_t类型的数据类型作为新线程的标识.

第二个参数:对程序的属性进行设置

第三个参数:线程将要启动执行的函数,该函数的返回值和参数都是void指针,这样就可以传递任意类型的指针

第四个参数:传递给线程将要执行的函数(第三个参数)的参数

(2) pthread_exit函数

#include <pthread.h>void pthread_exit (void *  retval );

线程在结束时必须调用pthread_exit函数,这与一个进程在结束时要调用exit是同样的道理。

返回值:返回一个指向某个对象的指针,绝不要用它返回一个指向一个局部变量的指针,因为局部变量会在线程出现严重问题时消失得无影无踪。

(3) pthread_join函数

#include <pthread.h>int pthread_join  (pthread_t th,  void ** thread_return  );

pthread_join相当于进程用来等待子进程的wait函数,它的作用是在线程结束后把它们归并到一起。

返回值:成功时返回“0”, 失败时返回一个错误代码

第一个参数:将要等待的线程,它就是pthread_create返回的那个标识符

第二个参数:是一个指针,它指向另外一个指针,而这个指针指向线程的返回值

简单实例:
thread.c文件

#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <pthread.h>void *thread_function(void*arg);/*message是共享的数据*/char message[] = "Hello World";int main(){ int res; pthread_t a_thread; void *thread_result; /*NULL表示不修改线程的属性,a_thread是新线程的标识符,以后的对新线程的引用就使用这个标识符*/ res = pthread_create(&a_thread, NULL, thread_function,(void*)message); if(res != 0){  perror("Thread creation failed");  exit(EXIT_FAILURE);  } printf("waiting for thread to finish...\n"); /*等待新线程执行完,然后合并新线程,thread_result是新线程的返回值,        这里是"Thank you for the CPU time",即pthread_exit的参数内容,这个函数会等到新线程结束后才返回*/ res = pthread_join(a_thread, &thread_result); if(res != 0){  perror("THread join failed");  exit(EXIT_FAILURE);   } printf("Thread joined, it returned %s\n",(char*)thread_result); printf("Message is now %s\n", message); exit(EXIT_SUCCESS);}void *thread_function(void* arg){ printf("thread_function is running. Argument was %s\n",(char*)arg); sleep(3); strcpy(message, "Bye"); pthread_exit("Thank you for the CPU time"); }

程序调用了pthread_create后,新线程开始执行。就是说,调用成功后,我们就有两个线程在运行。
原先的老线程将执行pthread_create后的代码,而新线程就去执行thread_function函数。
一开始,message是“Hello World”,但在新线程里,message被改成“Bye”。新线程结束后,输出的message依然是“Bye.”,因为message是共享的数据。

这里写图片描述

使用互斥量进行同步

互斥

简单地理解就是,一个线程进入工作区后,如果有其他线程想要进入工作区,它就会进入等待状态,要等待工作区内的线程结束后才可以进入。

基本函数

(1) pthread_mutex_init函数

原型:int pthread_mutex_init ( pthread_mutex_t *mutex, const pthread_mutexattr_t* attr);

描述:设置互斥量的属性

参数:第一个参数:预先声明的pthread_mutex_t对象指针

第二个参数:互斥锁属性,NULL表示使用默认属性

返回值:成功时返回0, 失败时返回一个错误代码

(2) pthread_mutex_lock函数

原型:int pthread_mutex_lock ( pthread_mutex_t *mutex );

描述:pthread_mutex_lock返回时,互斥锁被锁定,如果这个互斥锁被一个线程锁定和拥有,那么另一个线程要调用这 个函数会进入堵塞状态(即等待状态),直到互斥锁被释放为止。

返回值:成功时返回0, 失败时返回一个错误代码

(3) pthread_mutex_unlock函数

原型:int pthread_mutex_unlock ( pthread_mutex_t *mutex );

描述:释放互斥锁

返回值:成功时返回0, 失败时返回一个错误代码

(4) pthread_mutex_destroy函数

原型:int pthread_mutex_destroy ( pthread_mutex_t *mutex );

描述:删除互斥锁

返回值:成功时返回0, 失败时返回一个错误代码

实例
lock.c文件
描述:这个程序主要可以概括为主线程负责接受输入的字符串,而子线程则负责统计并输出字符数。

#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <pthread.h>#include <semaphore.h>void *thread_function(void *arg);pthread_mutex_t work_mutex;  #define WORK_SIZE 1024char work_area[WORK_SIZE];int time_to_exit = 0;int main() {    int res;    pthread_t a_thread;    void *thread_result;    /*初始化互斥量*/    res = pthread_mutex_init(&work_mutex, NULL);    if (res != 0) {        perror("互斥量初始化失败!");        exit(EXIT_FAILURE);    }    /*启动新线程*/    res = pthread_create(&a_thread, NULL, thread_function, NULL);    if (res != 0) {        perror("线程创建失败");        exit(EXIT_FAILURE);    }    pthread_mutex_lock(&work_mutex);    printf("请输入一些文本内容. 输入“end”结束\n");    while(!time_to_exit) {        fgets(work_area, WORK_SIZE, stdin);        pthread_mutex_unlock(&work_mutex);        while(1) {            pthread_mutex_lock(&work_mutex);            /*统计字符工作未完成*/            if (work_area[0] != '\0') {                pthread_mutex_unlock(&work_mutex);                sleep(1);            }            else {                /*统计字符工作完成,跳出内层循环,重新读取输入*/                break;            }        }    }    pthread_mutex_unlock(&work_mutex);    printf("\n等待线程结束...\n");    res = pthread_join(a_thread, &thread_result);    if (res != 0) {        perror("Thread join failed");        exit(EXIT_FAILURE);    }    printf("Thread joined\n");    pthread_mutex_destroy(&work_mutex);    exit(EXIT_SUCCESS);}/*主线程首先锁定工作区,在获取输入的字符后,释放工作区,让其他线程对字符个数进行统计。work_area[0[为空字符时表示统计结束。通过周期性地对互斥量进行加锁,检查是否已经统计完。*//*在线程中要执行的代码*/void *thread_function(void *arg) {    sleep(1);    pthread_mutex_lock(&work_mutex);    while(strncmp("end", work_area, 3) != 0) {        printf("你输入了 %d 个字符\n", strlen(work_area) -1);        work_area[0] = '\0';        pthread_mutex_unlock(&work_mutex);        sleep(1);        pthread_mutex_lock(&work_mutex);        while (work_area[0] == '\0' ) {            pthread_mutex_unlock(&work_mutex);            sleep(1);            pthread_mutex_lock(&work_mutex);        }    }    time_to_exit = 1;    work_area[0] = '\0';    pthread_mutex_unlock(&work_mutex);    pthread_exit(0);} 

在新线程一上来先试图对互斥量进行加锁。如果它已经被锁上,新线程就会进入堵塞状态知道互斥锁释放为止,一旦可以进入工作区,就先检查是否有退出请求(end)如果有,就设置time_to_exit变量和work_area,然后退出程序。
如果没有退出,那么就对字符个数进行统计。把work_area[0]设置为空,表示统计工作完成。接下来就释放互斥锁,等待主线程的运行,周期性地给互斥量加锁,如果加锁成功,就检查主线程是否又给我们新的字符串统计。如果没有,就释放互斥锁继续等待。
这里写图片描述

0 0
原创粉丝点击