5.1.1 进程概念
20世纪60年代,进程(process)一词首先在麻省理工学院的MULTICS和IBM的CTSS/360系统中被引入。
对进程下个准确定义不容易,一般的我们认为进程是一个程序的一次执行过程。进程是申请系统资源的基本的单位,它具有的两个重要特性。
1. 独立性
进程是系统中独立存在的实体,它可以拥有自己独立的资源,比如文件和设备描述符等。
在没有经过进程本身允许的情况下,其他进程不能访问到这些资源。这一点上和线程有很大的不同。
线程是共享资源的程序实体,创建一个线程所花费的系统开销要比创建一个进程小得多。
2. 动态性
进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。
在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
1. 独立性
2. 动态性
5.1.2
图 5-1 进程的数据结构
5.1.3
◆ 就绪(ready)态:
指进程已经获得所有所需的其他资源,并正在申请处理机资源,准备开始运行。
这种情况下,称进程处于就绪态。
◆ 阻塞(blocked)态:
指进程因为需要等待所需资源而放弃处理机,或者进程本不拥有处理机,且其他资源也没有满足,从而即使得到处理机资源也不能开始运行。
这种情况下,称进程处于阻塞态。阻塞状态又称休眠状态或者等待状态。
◆ 运行态:
进程得到了处理机,并不需要等待其他任何资源,正在执行的状态,称之为运行态。
只有在运行态时,进程才可以使用所申请到的资源。
图 5-2 Linux
◆ RUNNING:
正在运行,或者在就绪队列中等待运行的进程。
也就是上面提到的运行态和就绪态进程的综合。
一个进程处于RUNNING状态,并不代表它一定在被执行。
◆ UNINTERRUPTABLE:
不可中断阻塞状态。
处于这种状态的进程正在等待队列中,当资源有效时,可由操作系统进行唤醒,否则,将一直处于等待状态。
◆ STOPPED:
挂起状态。
进程被暂停,需要通过其他进程的信号才能被唤醒。
导致这种状态的原因有两种。
其一是受到了相关信号(SIGSTOP、SIGSTP、SIGTTIN 或SIGTTOU)的反应;
其二是受到父进程ptrace调用的控制,而暂时将处理机交给控制进程。
◆ ZOMBIE:
僵尸状态。
表示进程结束但尚未消亡的一种状态。
此时进程已经结束运行并释放大部分资源,但尚未释放进程控制块。
5.2
进程的管理
5.2.1
进程调度
调度程序(scheduler)用来实现进程状态之间的转换。
在Linux中,调度程序由系统调用schedule()来完成。
schedule()是一个怪异的函数,它与一般C语言函数不同,因为它的调用和返回不在同一个进程中。
5.2
5.2.1
5.2.2
1. Linux系统信号
5-3 Linux 系统信号
2. 信号的产生条件
(1)
(2)
(3)
kill(2)系统调用可允许进程向其他进程或进程组发送任意信号。
(4) kill(1)命令允许用户向进程发送任意信号。
(5) 软件设置的条件,如SIGALARM。
(4)
(5)
(1) 捕捉信号
它可以决定系统对信号的响应。
(2) 发送信号
① kill和raise
int kill(pid_t pid,int sig);向其他进程发送信号
int raise(int sig);向当前进程发送信号
② alarm
unsigned int alarm(unsigned int seconds);
这个函数可以用来设置一个时间值(闹钟时间),当所设置的值被超过以后,产生SIGALRM信号,默认动作是终止进程。
③ pause
int pause(void);
可以使进程挂起,直到捕捉到一个信号。执行了一个信号处理函数后pause函数返回,错误返回-1,errno设为EINTR。
④ sleep
unsigned int sleep(unsigned int seconds);
此函数挂起调用中的进程,直到过了预定时间或者是收到一个信号并从信号处理程序返回。
例:程序执行2秒打印hello字符串
5.3
图 5-4 父子进程关系
有些系统为了克服fork调用的缺点,创建了vfork调用。用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,子进程对虚拟地址空间任何数据的修改同样为父进程所见。但是用vfork创建子进程后,父进程会被阻塞直到子进程调用exec或exit。这样的好处是在子进程被创建后仅仅是为了调用exec执行另一个程序时,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的,通过vfork可以减少不必要的开销。
5.4 Linux进程控制编程
5.4.1 进程控制编程基础
(1)进程创建
fork()的语法要点
所需头文件
函数原型
返回值
0:子进程
子进程ID(大于0的整数):父进程
-1:出错
fork使用示例
(2)exec函数族
一般在调用fork()函数以后,可以用exec函数族来启动另一个程序的执行。exec函数族可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完后,原调用进程的内容除了进程号外,其他的全部被新的进程替换了。
exec函数族语法
所需头文件
#include<unistd.h>
函数原型
int execl(const char *path, const char *arg, ... );
int execv(const char *path, char *const argv[]);
int execle(const char *path,
const char *arg, ... ,char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int execlp(const char *file, const char *arg, ... );
int execvp(const char *file, char *const argv[]);
返回值:如果出错则返回-1,否则不返回。
所需头文件
#include<unistd.h>
函数原型
int execl(const char *path, const char *arg, ... );
int execv(const char *path, char *const argv[]);
int execle(const char *path,
int execve(const char *path, char *const argv[], char *const envp[]);
int execlp(const char *file, const char *arg, ... );
int execvp(const char *file, char *const argv[]);
返回值:如果出错则返回-1,否则不返回。
示例:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
if(fork()==0){
if(execlp("ps","ps","-ef",NULL)<0)
perror("execlp error!");
}
}
{
}
(3)exit和_exit
exit和_exit函数都可以用来终止一个进程。区别是:_exit立即进入内核,exit则先执行一些清除处理(包括调用执行各终止处理程序,关闭所有标准I/O流等),然后进入内核。
所需头文件:
exit:#include<stdlib.h>
_exit: #include<unistd.h>
函数原型:
exit: void exit(int status)
_exit: void _exit(int status)
所需头文件:
exit:#include<stdlib.h>
_exit: #include<unistd.h>
函数原型:
exit: void exit(int status)
_exit: void _exit(int status)
示例:
1)使用exit:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("Using exit...\n");
printf("This is the content in buffer");
exit(0);
}
1)使用exit:
#include <stdio.h>
#include <stdlib.h>
int main()
{
}
该程序的输出为:
Using exit...
This is the content in buffer
Using exit...
This is the content in buffer
2)使用_exit:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("Using _exit...\n");
printf("This is the content in buffer");
_exit(0);
}
#include <stdio.h>
#include <unistd.h>
int main()
{
}
该程序的输出为:
Using _exit...
Using _exit...
(4)wait和waitpid函数
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait (int * status);
wait()会暂时停止目前进程的执行,直到有信号来到或子进程结束。如果在调用wait()时子进程已经结束,则wait()会立即返回子进程结束状态值。子进程的结束状态值会由参数status返回,而子进程的进程识别码也会一快返回。如果不在意结束状态值,则参数status可以设成NULL。子进程的结束状态值请参考waitpid()。
如果执行成功则返回子进程识别码(PID),如果有错误发生则返回-1。失败原因存于errno中。
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait (int * status);
pid_t waitpid(pid_t pid,int * status,int options);
waitpid()会暂时停止目前进程的执行,直到有信号来到或子进程结束。如果在调用wait()时子进程已经结束,则wait()会立即返回子进程结束状态值。子进程的结束状态值会由参数status返回,而子进程的进程识别码也会一快返回。如果不在意结束状态值,则参数status可以设成NULL。参数pid为欲等待的子进程识别码,其他数值意义如下:
pid<-1
pid=-1
pid=0
pid>0
waitpid使用示例
本例中首先使用fork新建一个子进程,然后让子进程暂停5秒。接下来对父进程使用waitpid函数,并使用参数WNOHANG使该父进程不会阻塞。父进程每隔一秒循环判断一次,如果子进程没有退出,则显示相关消息,如果子进程退出了,则程序结束。
该程序的运行结果为:
The child process has not exited
The child process has not exited
The child process has not exited
The child process has not exited
The child process has not exited
Get child 75
如果把“pr=waitpid(pc,NULL,WNOHANG)”改为“pr=waitpid(pc,NULL,0)”或者“pr=wait(NULL)”,则运行结构为:
Get child 76
5.4.2 Linux守护进程编程
守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。 守护进程通常是在系统引导时启动,在系统关闭时终止。linux系统有很多守护进程,大多数服务都是用守护进程实现的。比如,作业规划进程crond、打印进程lqd等。守护进程可以Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(通常是shell)执行。
The child process has not exited
The child process has not exited
The child process has not exited
The child process has not exited
The child process has not exited
Get child 75
如果把“pr=waitpid(pc,NULL,WNOHANG)”改为“pr=waitpid(pc,NULL,0)”或者“pr=wait(NULL)”,则运行结构为:
Get child 76
5.4.2 Linux守护进程编程
守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。 守护进程通常是在系统引导时启动,在系统关闭时终止。linux系统有很多守护进程,大多数服务都是用守护进程实现的。比如,作业规划进程crond、打印进程lqd等。守护进程可以Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(通常是shell)执行。
进程组和会话期
进程组 每个进程除了有一进程ID之外,还属于一个进程组(在讨论信号时就会涉及进程组)进程组是一个或多个进程的集合。每个进程有一个唯一的进程组ID。进程组ID类似于进程ID——它是一个正整数,并可存放在pid_t数据类型中。可以调用getpgrp()查看当前进程的进程组ID。 每个进程组有一个组长进程。组长进程的标识是其进程组ID等于其进程ID,进程组组长可以创建一个进程组,创建该组中的进程,然后终止,只 要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。从进程组创建开始到其中最后一个进程离开为止的时间区间称为进程组的生命 期。某个进程组中的最后一个进程可以终止,也可以参加另一进程组。
进程组 每个进程除了有一进程ID之外,还属于一个进程组(在讨论信号时就会涉及进程组)进程组是一个或多个进程的集合。每个进程有一个唯一的进程组ID。进程组ID类似于进程ID——它是一个正整数,并可存放在pid_t数据类型中。可以调用getpgrp()查看当前进程的进程组ID。 每个进程组有一个组长进程。组长进程的标识是其进程组ID等于其进程ID,进程组组长可以创建一个进程组,创建该组中的进程,然后终止,只 要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。从进程组创建开始到其中最后一个进程离开为止的时间区间称为进程组的生命 期。某个进程组中的最后一个进程可以终止,也可以参加另一进程组。
会话期 会话期(session)是一个或多个进程组的集合。
一个会话期可以有一个单独的控制终端(controlling terminal),这一般是我们在其上登录的终端设备(终端登录)或伪终端设备(网络登录),但这个控制终端并不是必需的。
建立与控制终端连接的会话期首进程,被称之为控制进程(contronlling process)。
一个会话期中的几个进程组可被分为 一个前台进程组(foreground process
group)以及一个或几个后台进程组(background
process
group)
如果一个会话期有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组。无论何时键入中断键(常常是delete或ctrl-c)或退出键(通常是ctrl-\),就会造成将中断信号或退出信号送至前台进程组的所有进程。
一个会话期可以有一个单独的控制终端(controlling
建立与控制终端连接的会话期首进程,被称之为控制进程(contronlling
一个会话期中的几个进程组可被分为 一个前台进程组(foreground
如果一个会话期有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组。无论何时键入中断键(常常是delete或ctrl-c)或退出键(通常是ctrl-\),就会造成将中断信号或退出信号送至前台进程组的所有进程。
守护进程的编写规则
守护进程比较复杂,但是只要掌握其编写的一般流程,我们也可以很方便的编写自己的守护进程。
编写守护进程一般的步骤如下所述:
1)创建子进程,父进程退出 为避免挂起控制终端,要将daemon放入后台执行,其方法是,在进程中调用fork使父进程终止,让daemon在子进程中后台执行。具体就是调用f o
r
k
,然后使父进程e
x
i
t
。
pid=fork();
if(pid>0) {
exit(0);
}
1)创建子进程,父进程退出 为避免挂起控制终端,要将daemon放入后台执行,其方法是,在进程中调用fork使父进程终止,让daemon在子进程中后台执行。具体就是调用f
2)在子进程中创建新的会话期
这是创建守护进程中最重要的一步,需要使用setsid这个函数。
setsid函数的语法
所需头文件:
#include<sys/types.h>
#include<unistd.h>
函数原型:
pid_t setsid(void)
返回值:
成功:该进程组ID
错误:-1
setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离,由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
setsid函数的语法
所需头文件:
#include<sys/types.h>
#include<unistd.h>
函数原型:
pid_t setsid(void)
返回值:
成功:该进程组ID
错误:-1
3)改变当前目录为根目录
从父进程继承过来的当前工作目录可能在一个可卸载的文件系统中。因为守护进程通常在系统再引导之前是一直存在的,所以如果精灵进程的当前工作目录在一个可卸载文件系统中,那么该文件系统就不能被拆卸,这样可能会出现某些不便。所以一般在守护经常中把当前工作目录更改为根目录,这样就可以避免上述问题。改变工作目录可以使用chdir函数。
#include<unistd.h>
int chdir(const char *path)
返回值:成功:0;
不成功:-1;
4)重设文件权限掩码
文件权限掩码是只屏蔽掉文件权限中对应位。由继承得来的文件方式创建屏蔽字可能会拒绝设置某些许可权。例如,若精灵进程要创建一个组可读、写的文件,而继承的文件方式创建屏蔽字,屏蔽了这两种许可权,则所要求的组可读、写就不能起作用。 因此我们一般把文件权限掩码设置为0,这样就可以读写所有文件。设置文件权限掩码的函数是umask;
#include<sys/types.h>
#include<sys/stat.h>
mode_t umask(mode_t mask)
返回值:这个函数总是执行成功并返回执行前的文件权限掩码。
#include<sys/types.h>
#include<sys/stat.h>
mode_t umask(mode_t mask)
返回值:这个函数总是执行成功并返回执行前的文件权限掩码。
5)关闭打开的文件描述符
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在地文件系统无法卸下以及无法预料的错误。一般来 说,必要的是关闭0、1、2三个文件描述符,即标准输入、标准输出、标准错误。因为我们一般希望守护进程自己有一套信息输出、输入的体系,而不是把所有的 东西都发送到终端屏幕上。调用fclose();
守护进程编程示例
本例首先创建一个守护进程,然后让守护进程每隔10s在/tmp/dameon.log中写入一句话;
守护进程示例源代码(dameon.c)
守护进程示例源代码(dameon.c)
守护进程的出错处理
守护进程不属于任何终端,所以当需要输出某些信息时,它无法像一般程序那样将信息直接输出到标准输 出和标准错误输出中。我们很大时候也不希望每个守护进程将它自己的出错消息写到一个单独的文件中。因为对于系统管理人员而言,要记住哪一个守护进程写到哪 一个记录文件中,并定期的检查这些文件,他一定会为此感到头疼的。所以,我们需要有一个集中的守护进程出错记录机制。目前很多系统都引入了syslog记 录进程来实现这一目的。
Syslog是Linux系统中的系统日志管理服务,通常由守护进程syslogd来维护。此守护进程在启动时读一个配置文件。一般来说,其文件名为/etc/syslog.conf,该文 件决定了不同种类的消息应送向何处。例如,紧急消息可被送向系统管理员(若已登录),并在控制台上显示,而警告消息则可记录到一个文件中。该机制提供了 syslog函数,其调用格式如下
#include
<syslog.h> void
openlog
(char*ident,int
option
,int
facility); void
syslog(int
priority,char*format,……) void
closelog(); 调用openlog是可选择的。如果不调用openlog,则在第一次调用syslog时,自动调用openlog。调用closelog也 是可选择的,它只是关闭被用于与syslog守护进程通信的描述符。调用openlog
使我们可以指定一个ident,以后,
此ident
将被加至 每则记录消息中。ident
一般是程序的名称(例如
,cron
,inetd
等)。
option
有4种可能: LOG_CONS
若日志消息不能通过Unix域数据报发送至syslog,则将该消息写至控制台。 LOG_NDELAY1
立即打开Unix域数据报套接口至syslog守护进程,而不要等到记录第一消息。通常,在记录第一条消息之前,该套接口不打开。
LOG_PERROR
除将日志消息发送给syslog
外,还将它至标准出错。此选项仅由4.3BSDReno及以后版本支持。 LOG_PID
每条消息都包含进程ID。此选项可供对每个请求都fork一个子进程的守护进程使用。
acility facility参数用来指定何种程式在记录讯息,这可让设定档来设定何种讯息如何处理。 LOG_AUTH : 安全/授权讯息(别用这个,请改用LOG_AUTHPRIV) LOG_AUTHPRIV : 安全/授权讯息 LOG_CRON : 时间守护进程专用(cron及at) LOG_DAEMON : 其它系统守护进程 LOG_KERN : 核心讯息 LOG_LOCAL0到LOG_LOCAL7 : 保留 LOG_LPR : line printer次系统 LOG_MAIL : mail次系统 LOG_NEWS : USENET news次系统 LOG_SYSLOG : syslogd内部所产生的讯息 LOG_USER(default) : 一般使用者等级讯息 LOG_UUCP : UUCP次系统
在
syslog(int
priority,char*format,……)中:
priotity
决定讯息的重要性. 以下的等级重要性逐次递减: LOG_EMERG : 系统无法使用 LOG_ALERT : 必须要立即采取反应行动 LOG_CRIT : 重要状况发生 LOG_ERR : 错误状况发生 LOG_WARNING : 警告状况发生 LOG_NOTICE : 一般状况,但也是重要状况 LOG_INFO : 资讯讯息 LOG_DEBUG : 除错讯
format
以字符川指针的形式表示的输出格式。
priotity
决定讯息的重要性. 以下的等级重要性逐次递减: LOG_EMERG : 系统无法使用 LOG_ALERT : 必须要立即采取反应行动 LOG_CRIT : 重要状况发生 LOG_ERR : 错误状况发生 LOG_WARNING : 警告状况发生 LOG_NOTICE : 一般状况,但也是重要状况 LOG_INFO : 资讯讯息 LOG_DEBUG : 除错讯
format
以字符川指针的形式表示的输出格式。
syslog使用示例
本例中对上例程序使用syslog服务进行重写,其源代码为syslog_dema.c
5.5
进程通讯
5.5.1
进程间通信
用户态进程间处于并发状态。
为了协调进程的运行,需要实现进程之间通信的机制。
本例中对上例程序使用syslog服务进行重写,其源代码为syslog_dema.c
5.5
5.5.1
Linux下的进程通信手段基本上是从Unix 平台上的进程通信手段继承而来的。而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中 心)在进程间通信方面的侧重点有所不同。前者对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了“system V IPC”,通信进程局限在单个计算机内;后者则跳过了该限制,形成了基于套接口(socket)的进程间通信机制。Linux则把两者继承了下来,如图 示:
在Linux中,常见的进程间通信方法包括:
1).管道机制。
该机制最适用于解决生产者――消费者问题。
管道是一种在进程之间单向流动数据的结构,具有固定的读端fd[0]和写端fd[1]。
管道只能用在具有亲缘关系的进程之间通信。
管道可以看成特殊文件,我们可以用read write等普通函数对其进行操作,但是它只存在于内存中。
管道的创建和关闭
创建函数: int pipe(int fd[2])
关闭:关闭管道只需要使用close函数关闭其打开的文件描述符即可。
管道读写需要注意的问题
管道在写入时不会保证写入操作的原子性。
虽然通过pipe()调用会产生两个描述符(写管道和读管道),但是在写之前必须关闭读管道,反之亦然。
创建函数: int pipe(int fd[2])
关闭:关闭管道只需要使用close函数关闭其打开的文件描述符即可。
管道读写需要注意的问题
管道在写入时不会保证写入操作的原子性。
虽然通过pipe()调用会产生两个描述符(写管道和读管道),但是在写之前必须关闭读管道,反之亦然。
管道使用示例:
本例中,首先创建管道,之后创建一个子进程,然后关闭父进程的读描述符和子进程的写描述符,建立他们之间的管道通信。
源代码(pipe_rw.c)
2). 先进先出(FIFO)机制。
管道机制的最大缺点是不能由多个进程共享,除非此管道为这些进程共同的祖先所创建。
为了解决这个问题,Linux中引入了FIFO机制(又称为named pipe,有名管道)。
mkfifo函数(创建有名管道)
所需头文件:
#include<sys/types.h>
#include<sys/stat.h>
函数原型:
int mkfifo(const char * pathname,mode_t mode);
mkfifo()会依参数pathname建立特殊的FIFO文件,该文件必须不存在,而参数mode为该文件的权限。
返回值:成功 0;出错 -1,错误原因保存在errno中
所需头文件:
函数原型:
返回值:成功 0;出错 -1,错误原因保存在errno中
管道创建成功后就可以使用open、read、write等函数进行操作。需要注意的一点是FIFO的读写是可以阻塞的(在open函数中可设定非阻塞标志O_NONBLOCK)。如果FIFO是阻塞打开,对于读进程,若FIFO中没有数据可读,则读进程会被阻塞;对于写进程,则写进程会被阻塞直到有读进程读出数据。
使用示例
本例中包含两个程序,一个用于读管道(fifo_write.c),一个用户写管道(fifo_read.c),两个程序都是使用非阻塞方式读写管道。
本例中包含两个程序,一个用于读管道(fifo_write.c),一个用户写管道(fifo_read.c),两个程序都是使用非阻塞方式读写管道。
3. IPC机制。
IPC是“inter process communication”的缩写形式。
它包含了一系列系统调用,允许用户态进程通过信号量进行同步,向其他进程发消息,并且可以与其他进程共享一块内存空间.
IPC首先是在一个叫做“Columbus Unix”的系统中实现的,其后在现代Unix类操作系统中广为流行。
(1) 消息队列
(2) 共享内存
用共享内存实现过程如下:
① 服务器取得访问该共享内存区的权限。
② 服务器从输入文件读取数据到该共享内存区。
③ 服务器读入数据完毕时,通知用户进程。
④ 用户从该共享内存区读出这些数据并输出。
(3) 信号量
信号量主要包括以下几种类型:
① 二值信号量:其值为0或1。资源被锁住而不可用时,值为0;资源可用时,值为1。
② 计数信号量:其值在0和某个限制值之间。它的值一般是可用的资源数。
③ 计数信号量集:由一个或多个信号量构成的一个集合,其中每一个都是计数信号量。每个集合的信号量数都有一个限制值,一般在25个以上。
5.6
5.6.1
线程和进程相比有以下优点:
1.“节俭”的多任务操作方式
2. 线程间方便的通信机制
3. 提高应用程序响应
4. 使多CPU系统更加有效
5. 改善程序结构
6. 数据共享问题
5.6.2
1. 线程ID
2. 寄存器组的值
由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。
3. 线程的堆栈
堆栈是保证线程独立运行所必须的。
线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈,使得函数调用可以正常执行,不受其他线程的影响。
4. 错误返回码
由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了errno值,而在该线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。
所以,不同的线程应该拥有自己的错误返回码变量。
5. 线程的信号屏蔽码
由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。
但所有的线程都共享同样的信号处理器。
6. 线程的优先级
由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。
5.6.3
线程的分类
从调度的角度,线程可以分为用户线程和内核线程两类:
1.用户线程
用户线程是通过线程库实现的。它们可以在没有内核参与下创建、释放和管理。线程库提供了同步和调度的方法。这样进程可以使用大量的线程而不消耗内核资源,而且省去大量的系统开销。用户线程的实现是可能的,因为用户线程的上下文可以在没有内核干预的情况下保存和恢复。每个用户线程都可以有自己的用户堆栈,一块用来保存用户级寄存器上下文以及如信号屏蔽等状态信息的内存区。库通过保存当前线程的堆栈和寄存器内容载入新调度线程的那些内容来实现用户线程之间的调度和上下文切换。
2.内核线程
它的创建和撤消是由内核的内部需求来决定的,用来负责执行一个指定的函数,一个内核线程不需要和一个用户进程联系起来。它共享内核的正文段核全局数据,具有自己的内核堆栈。它能够单独的被调度并且使用标准的内核同步机制,可以被单独的分配到一个处理器上运行。内核线程的调度由于不需要经过态的转换并进行地址空间的重新映射,因此在内核线程间做上下文切换比在进程间做上下文切换快得多。
5.7
线程编程基础
本节将结合实例说明多线程编程中所使用到的一些函数和基本方法。
5.7.1
线程的基本操作函数
以下先讲述4个基本线程函数,在调用它们前均要包括pthread.h头文件。
然后再给出用它们编写的一个程序例子。
1. 创建线程函数
int pthread_create(pthread_t *tid,const pthread_attr_t *attr,
void *(*func)(void *),void *arg);
4.
5.
6.
5.6.3
1.用户线程
2.内核线程
5.7
5.7.1
1. 创建线程函数
2. 等待线程的结束函数
3. 取自己线程ID函数:
4. 终止线程函数
5.7.2
5.7.3 修改线程属性
属性结构为pthread_attr_t,它在头文件/usr/include/bits/ pthreadtypes.h中定义。属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。
typedef struct __pthread_attr_s
{
} pthread_attr_t;
__detachstate,表示新线程是否与进程中其他线程脱离分离,如果置位则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。缺省为PTHREAD_CREATE_JOINABLE状态。这个属性也可以在线程创建并运行以后用pthread_detach()来设置,而一旦设置为PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE状态。该参数涉及函数:
int pthread_attr_setdetachstate (pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate (const pthread_attr_t *attr,
int *detachstate);
__detachstate可取值:PTHREAD_CREATE_JOINABLE, PTHREAD_CREATE_DETACH。
int pthread_attr_setdetachstate (pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate (const pthread_attr_t *attr,
__detachstate可取值:PTHREAD_CREATE_JOINABLE, PTHREAD_CREATE_DETACH。
int pthread_attr_setschedpolicy (pthread_attr_t *attr, int policy);
int pthread_attr_getschedpolicy (const pthread_attr_t *attr, int *policy);
__schedparam,一个struct sched_param结构,其中有一个sched_priority整型变量表示线程的运行优先级。这个参数仅当调度策略为实时(即SCHED_RR或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0。该参数涉及函数:
int pthread_attr_setschedparam (pthread_attr_t *attr,
const struct sched_param *param);
int pthread_attr_getschedparam (const pthread_attr_t *attr, struct sched_param *param);
int pthread_attr_setschedparam (pthread_attr_t *attr,
int pthread_attr_getschedparam (const pthread_attr_t *attr, struct sched_param *param);
int pthread_attr_setinheritsched (pthread_attr_t *attr,
int pthread_attr_getinheritsched (const pthread_attr_t *attr, int *inherit);
__scope,表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值:PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同进程中的线程竞争CPU。该参数涉及函数:
int pthread_attr_setscope (pthread_attr_t *attr, int scope);
int pthread_attr_getscope (const pthread_attr_t *attr, int *scope);
int pthread_attr_setscope (pthread_attr_t *attr, int scope);
int pthread_attr_getscope (const pthread_attr_t *attr,
pthread_attr_t结构中还有一些值,但不使用pthread_create()来设置。
另外还有pthread_attr_init(pthread_attr_t *attr) ,该函数用来初始化线程属性;pthread_attr_destroy(pthread_attr_t *attr),该函数使线程属性无效。
另外还有pthread_attr_init(pthread_attr_t *attr) ,该函数用来初始化线程属性;pthread_attr_destroy(pthread_attr_t *attr),该函数使线程属性无效。
属性设置示例
在本示例中(pthread.c)线程1设为分离属性,线程2使用默认属性(非分离)。该程序的运行结果如下:
This is a pthread1.
This is a pthread2.
This is a pthread2.
This is a pthread2.
5.7.4 线程访问控制
由于线程共享进程的资源和地址空间,因此在对这些资源进行操作时,必须考虑到线程间资源访问的唯一性。
在程序(threadrace.c)中,主线程和新线程都将全局变量 myglobal 加一 20 次。但是程序本身产生了某些意想不到的结果,最后输出的myglobal为20,而不是我们所预期的40;
在本示例中(pthread.c)线程1设为分离属性,线程2使用默认属性(非分离)。该程序的运行结果如下:
This is a pthread1.
This is a pthread2.
This is a pthread2.
This is a pthread2.
5.7.4 线程访问控制
由于线程共享进程的资源和地址空间,因此在对这些资源进行操作时,必须考虑到线程间资源访问的唯一性。
在POSIX中线程同步的方法主要有互斥锁和信号量。
一、互斥锁
mutex是一种简单的加锁方法来控制对共享资源的存取。它只有两种状态:上锁和解锁。在同一时刻只能有一个线程掌握某个已经上锁的互斥锁,拥有上锁状态的线程能够对共享资源进行操作。若其他的线程希望上锁一个已经上锁了的互斥锁,则该线程会被挂起,直到上锁的线程释放互斥锁为止。
一、互斥锁
1. 创建和销毁
有两种方法创建互斥锁,静态方式和动态方式。POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁,方法如下: pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; 在LinuxThreads实现中,pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个结构常量。
动态方式是采用pthread_mutex_init()函数来初始化互斥锁,API定义如下: int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr) 其中mutexattr用于指定互斥锁属性(见下),如果为NULL则使用缺省属性。
pthread_mutex_destroy()用于注销一个互斥锁,API定义如下: int pthread_mutex_destroy(pthread_mutex_t *mutex) 销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。
有两种方法创建互斥锁,静态方式和动态方式。POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁,方法如下: pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; 在LinuxThreads实现中,pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个结构常量。
动态方式是采用pthread_mutex_init()函数来初始化互斥锁,API定义如下: int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr) 其中mutexattr用于指定互斥锁属性(见下),如果为NULL则使用缺省属性。
pthread_mutex_destroy()用于注销一个互斥锁,API定义如下: int pthread_mutex_destroy(pthread_mutex_t *mutex) 销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。
2. Linux pthread互斥属性有三种
PTHREAD_MUTEX_INITIALIZER 快速互斥
PTHREAD_RECURSIVE_MUTEX_INITEALIZER_NP 递归互斥
PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP 检错互斥
PTHREAD_MUTEX_INITIALIZER 快速互斥
PTHREAD_RECURSIVE_MUTEX_INITEALIZER_NP 递归互斥
PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP 检错互斥
3. 锁操作
锁操作主要包括加锁pthread_mutex_lock()、解锁pthread_mutex_unlock()和测试加锁pthread_mutex_trylock()三个,不论哪种类型的锁,都不可能被两个不同的线程同时得到, 而必须等待解锁。
int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)
pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时 返回EBUSY而不是挂起等待。
int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)
mutex使用示例
在前面的示例(threadrace.c)中,由于没有控制主线程和新线程对共享资源的访问,我们没有得到预期的结果。现在我们使用互斥锁机制来修改该程序,修改后的代码如(threadracemutex.c)所示,这样我们就可以得到预期的结果。
在前面的示例(threadrace.c)中,由于没有控制主线程和新线程对共享资源的访问,我们没有得到预期的结果。现在我们使用互斥锁机制来修改该程序,修改后的代码如(threadracemutex.c)所示,这样我们就可以得到预期的结果。
二、信号量(信号灯)
信号量本质上是一个非负的整数计数器,它可以用来控制对公共资源的访问,如果信号量的值大于0,则表示资源可用,否则表示资源不可用。
信号量可以用于进程或者线程之间的同步和互斥两种情况。如果用于互斥,一般只需要设置一个信号量sem,操作流程如图5-6所示。如果用于同步,一般需要设置多个信号量,并安排不同的初始值来实现他们之间的顺序执行,操作流程如图5-7所示。
1. 创建和注销
int sem_init(sem_t *sem, int pshared, unsigned int value) 这是创建信号灯的API,其中value为信号灯的初值,pshared表示是否为多进程共享而不仅 仅是用于一个进程。LinuxThreads没有实现多进程共享信号灯,因此所有非0值的pshared 输入都将使sem_init()返回-1,且置errno为ENOSYS。
int sem_destroy(sem_t * sem) 被注销的信号灯sem要求已没有线程在等待该信号灯,否则返回-1,且置errno为EBUSY。除 此之外,LinuxThreads的信号灯注销函数不做其他动作。
2. 点灯和灭灯
int sem_post(sem_t * sem) 点灯操作将信号灯值原子地加1,表示增加一个可访问的资源。
int sem_wait(sem_t * sem)
int sem_trywait(sem_t * sem)
sem_wait()为等待灯亮操作,等待灯亮(信号灯值大于0),然后将信号灯原子地减1,并 返回。sem_trywait()为sem_wait()的非阻塞版,如果信号灯计数大于0,则原子地减1并返 回0,否则立即返回-1,errno置为EAGAIN。
3. 获取灯值
int sem_getvalue(sem_t * sem, int * sval) 读取sem中的灯计数,存于*sval中,并返回0。
int sem_post(sem_t * sem) 点灯操作将信号灯值原子地加1,表示增加一个可访问的资源。
int sem_wait(sem_t * sem)
int sem_trywait(sem_t * sem)
3. 获取灯值
信号量使用示例
1)信号量互斥使用示例
把上例中的互斥锁机制改为信号量机制。源代码threadracesem.c
2)信号量同步使用示例
本例中通过使用两个信号量来实现两个线程间的同步,通过观察其运行结果,可以确定该程序实现了先运行线程二,再运行线程一。源代码sem_syn.c
1)信号量互斥使用示例
把上例中的互斥锁机制改为信号量机制。源代码threadracesem.c
2)信号量同步使用示例
本例中通过使用两个信号量来实现两个线程间的同步,通过观察其运行结果,可以确定该程序实现了先运行线程二,再运行线程一。源代码sem_syn.c