Linux C进程与多线程

进程和程序的区别

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


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

代码实例
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 1024
char 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
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值