赞
踩
本章重点学习如何通过Linux系统调用或C库函数获取系统信息,譬如获取系统时间、日期
以及设置系统时间、日期等;除此之外,还会学习Linux系统下的/proc虚拟文件系统,包括/proc 文件系统是什么以及如何从/proc文件系统中读取系统、进程有关信息。
系统调用uname()用于获取有关当前操作系统内核的名称和信息,原型如下:
#include <sys/utsname.h>
int uname(struct utsname *buf);
函数参数和返回值含义如下:
sysinfo系统调用可用于获取一些系统统计信息,其函数原型如下所示:
#include <sys/sysinfo.h>
int sysinfo(struct sysinfo *info);
函数参数和返回值含义如下:
此函数可用于单独获取Linux系统主机名,与struct utsname数据结构体中的nodename变量一样,原型如下:
#include <unistd.h>
int gethostname(char *name, size_t len);
函数参数和返回值含义如下:
sysconf()函数是一个库函数,可在运行时获取系统的一些配置信息,函数原型如下所示:
#include <unistd.h>
long sysconf(int name);
参数name指定了要获取哪个配置信息,可通过宏定义来获取配置信息。用的比较多的是_SC_PAGESIZE和_SC_CLK_TCK来获取系统页大小和系统节拍率。
Ubuntu系统下可通过“date”命令查看系统当前时间。时区信息通常以标准格式保存在一些文件当中,这些文件通常于/usr/share/zoneinfo目录下,该目录下的每一个文件(包括子目录下的文件)都包含了一个特定国家或地区内时区制度的相关信息,且往往根据城市或地区缩写来命名。
Linux系统在开机启动之后首先会读取RTC硬件获取实时时钟作为系统时钟的初始值,之后内核便开始维护自己的系统时钟。
jiffies是内核中定义的一个全局变量,内核使用jiffies来记录系统从启动以来的系统节拍数,操作系统使用jiffies这个全局变量来记录当前时间。
系统调用time()用于获取当前时间,以秒为单位,返回得到的值是自1970-01-01 00:00:00 +0000(UTC)以来的秒数,其函数原型如下所示:
#include <time.h>
time_t time(time_t *tloc);
函数参数和返回值含义如下:
系统调用gettimeofday()函数提供微秒级时间精度,函数原型如下所示:
#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
函数参数和返回值含义如下:
ctime()是一个C库函数,可以将日历时间转换为可打印输出的字符串形式,原型如下所示:
#include <time.h>
char *ctime(const time_t *timep);
char *ctime_r(const time_t *timep, char *buf);
函数参数和返回值含义如下:
ctime_r()是ctime()的可重入版本,一般推荐使用可重入函数ctime_r(),可重入函数ctime_r()多了一个参数buf,也就是缓冲区首地址,所以ctime_r()函数需要调用者提供用于存放字符串的缓冲区。
localtime()函数可以把time()或gettimeofday()得到的秒数(time_t 时间或日历时间)变成一个 struct tm结构体所表示的时间,该时间对应的是本地时间。原型如下:
#include <time.h>
struct tm *localtime(const time_t *timep);
struct tm *localtime_r(const time_t *timep, struct tm *result);
函数参数和返回值含义如下:
gmtime()函数也可以把time_t时间变成一个struct tm结构体所表示的时间,与localtime()所不同的是,gmtime()函数所得到的是UTC国际标准时间,原型如下:
#include <time.h>
struct tm *gmtime(const time_t *timep);
struct tm *gmtime_r(const time_t *timep, struct tm *result);
该函数的参数和返回值与localtime()是一样的。
C库函数mktime()函数与localtime()函数相反,mktime()可以将使用struct tm结构体表示的分解时间转换为time_t时间(日历时间),原型如下:
#include <time.h>
time_t mktime(struct tm *tm);
函数参数和返回值含义如下:
asctime()函数与ctime()函数的作用一样,也可将时间转换为可打印输出的字符串形式,与ctime()函数的区别在于,ctime()是将time_t时间转换为固定格式字符串、而asctime()则是将struct tm表示的分解时间转换为固定格式的字符串。原型如下所示:
#include <time.h>
char *asctime(const struct tm *tm);
char *asctime_r(const struct tm *tm, char *buf);
函数参数和返回值含义如下:
C库函数strftime(),此函数也可以将一个struct tm变量表示的分解时间转换为为格式化字符串,并且可以根据自己的喜好自定义时间的显示格式,原型如下:
#include <time.h>
size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);
函数参数和返回值含义如下:
settimeofday()函数可以设置时间,也就是设置系统的本地时间,函数原型如下所示:
#include <sys/time.h>
int settimeofday(const struct timeval *tv, const struct timezone *tz);
函数参数和返回值含义如下:
只有超级用户(root)才可以设置系统时间。
Linux系统下时间相关函数使用总结如下:
进程时间指的是进程从创建后(也就是程序运行后)到目前为止这段时间内使用CPU资源的时间总数,出于记录的目的,内核把CPU时间分为以下两个部分:
休眠并不会计算在进程时间中,因为并没有使用CPU资源。
times()函数用于获取当前进程时间,其函数原型如下所示:
#include <sys/times.h>
clock_t times(struct tms *buf);
函数参数和返回值含义如下:
库函数clock()提供了一个更为简单的方式用于进程时间,它的返回值描述了进程使用的总的CPU时间,其函数原型如下所示:
#include <time.h>
clock_t clock(void);
函数参数和返回值含义如下:
rand()函数用于获取随机数,多次调用rand()可得到一组随机数序列,其函数原型如下:
#include <stdlib.h>
int rand(void);
函数参数和返回值含义如下:
使用srand()函数为rand()设置随机数种子,其函数原型如下所示:
#include <stdlib.h>
void srand(unsigned int seed);
函数参数和返回值含义如下:
常用的用法srand(time(NULL));
sleep()是一个C库函数,原型如下:
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
函数参数和返回值含义如下:
usleep()同样也是一个C库函数,支持微秒级程序休眠,其函数原型如下所示:
#include <unistd.h>
int usleep(useconds_t usec);
函数参数和返回值含义如下:
系统调用nanosleep()具有更高精度来设置休眠时间长度,支持纳秒级时长设置,原型如下:
#include <time.h>
int nanosleep(const struct timespec *req, struct timespec *rem);
函数参数与返回值含义如下:
应用程序当中使用休眠用作延时功能,并不是裸机程序中的nop空指令延时,一旦执行sleep(),进程便主动交出CPU使用权,暂时退出系统调度队列,在休眠结束前,该进程的指令将得不到执行。
Linux C程序当中一般使用malloc()函数为程序分配一段堆内存,而使用free()函数来释放这段内存,malloc原型如下:
#include <stdlib.h>
void *malloc(size_t size);
函数参数和返回值含义如下:
通常需要对malloc()分配的堆内存进行初始化操作。
手动释放函数free()原型如下:
#include <stdlib.h>
void free(void *ptr);
函数参数和返回值含义如下:
calloc()函数用来动态地分配内存空间并初始化为0,其函数原型如下所示:
#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);
C函数库中还提供了一系列在堆上分配对齐内存的函数,对齐内存在某些应用场合非常有必要,常用于分配对其内存的库函数有posix_memalign()、aligned_alloc()、memalign()、valloc()、pvalloc(),它们的函数原型如下所示:
#include <stdlib.h>
int posix_memalign(void **memptr, size_t alignment, size_t size);
void *aligned_alloc(size_t alignment, size_t size);
void *valloc(size_t size);
#include <malloc.h>
void *memalign(size_t alignment, size_t size);
void *pvalloc(size_t size);
posix_memalign()函数用于在堆上分配size个字节大小的对齐内存空间,将*memptr指向分配的空间,分配的内存地址将是参数alignment的整数倍。参数alignment表示对齐字节数,alignment必须是2的幂次方,同时也要是sizeof(void *)的整数倍,对于32位系统来说,sizeof(void *)等于4,如果是64位系统sizeof(void *)等于8。
函数参数和返回值含义如下:
aligned_alloc()函数用于分配size个字节大小的内存空间,返回指向该空间的指针。
函数参数和返回值含义如下:
memalign()函数已经过时了,并不提倡使用!
valloc()函数已经过时了,并不提倡使用!
proc文件系统是一个虚拟文件系统,它以文件系统的方式为应用层访问系统内核数据提供了接口,用户和应用程序可以通过proc文件系统得到系统信息和进程相关信息,对proc文件系统的读写作为与内核进行通信的一种手段。但是与普通文件不同的是,proc文件系统是动态创建的,文件本身并不存在于磁盘当中、只存在于内存当中,与devfs一样,都被称为虚拟文件系统。
内核构建proc虚拟文件系统,它会将内核运行时的一些关键数据信息以文件的方式呈现在proc文件系统下的一些特定文件中,这样相当于将一些不可见的内核中的数据结构以可视化的方式呈现给应用层。
proc文件系统挂载在系统的/proc目录下,相当于提供了一种调试内核的方法:通过查看/proc/xxx文件来获取到内核特定数据结构的值,在添加了新功能前后进行对比,就可以判断此功能所产生的影响是否合理。
proc文件系统的使用就是去读取/proc目录下的这些文件,获取文件中记录的信息,可以直接使用cat命令读取,也可以在应用程序中调用open()打开、然后再使用read()函数读取。
在很多应用程序当中,都会存在处理异步事件这种需求,而信号提供了一种处理异步事件的方法。
信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟。大多数情况下,是无法预测信号达到的准确时间,所以,信号提供了一种处理异步事件的方法。
信号的目的就是用来通信的,在硬件发生异常、终端输入能产生信号的特殊字符、进程调用kill()系统调用、kill命令发送信号、软件事件等均可产生信号。
信号被对应进程接受后,可以忽略信号、捕捉信号、也可以执行系统默认操作。
信号是异步的,只有触发了信号才会打断当前程序正常执行流程转而处理信号触发后事件。
信号本质就是int类型数字编号。信号都定义在<signum.h>头文件中,编号从1开始。
Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用signal()。因此,Linux下的不可靠信号问题主要指的是信号可能丢失。在Linux系统下,信号值小于SIGRTMIN(34)的信号都是不可靠信号,这就是"不可靠信号"的来源,因此之前<signum.h>中的信号均为不可靠信号。
之后又新增加了许多信号(SIGRTMIN-SIGRTMAX),并定义为可靠信号,在Linux系统下可用“kill -l”查看所有信号,如下所示:
非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。实时信号保证了发送的多个信号都能被接收,实时信号是POSIX标准的一部分,可用于应用进程。
一般会将非实时信号(不可靠信号)称为标准信号。
当用户在终端按下中断字符(通常是CTRL + C)时,内核将发送SIGINT信号给前台进程组中的每一个进程。该信号的系统默认操作是终止进程的运行。
当用户在终端按下退出字符(通常是CTRL + )时,内核将发送SIGQUIT信号给前台进程组中的每一个进程。该信号的系统默认操作是终止进程的运行、并生成可用于调试的核心转储文件。一般用于进程陷入无限循环等情况下。
如果进程试图执行非法(即格式不正确)的机器语言指令,系统将向进程发送该信号。该信号的系统默认操作是终止进程的运行。
当进程调用abort()系统调用时(进程异常终止),系统会向该进程发送SIGABRT信号。该信号的系统默认操作是终止进程、并生成核心转储文件。
产生该信号(总线错误,bus error)表示发生了某种内存访问错误。该信号的系统默认操作是终止进程。
该信号因特定类型的算术错误而产生。该信号的系统默认操作是终止进程。
此信号为“必杀(sure kill)”信号,用于杀死进程的终极办法,此信号无法被进程阻塞、忽略或者捕获,总能终止进程。前提条件是该进程并没有忽略或捕获这些信号,如果使用SIGINT或SIGQUIT无法终止进程,那就使用SIGKILL。Linux下有一个kill命令,kill命令可用于向进程发送信号,会使用"kill -9 xxx"命令来终止一个进程(xxx 表示进程的pid),这里的-9其实指的就是发送编号为9的信号,也就是SIGKILL信号。
该信号和SIGUSR2信号供程序员自定义使用,内核绝不会为进程产生这些信号,在程序中,可以使用这些信号来互通通知事件的发生,或是进程彼此同步操作。该信号的系统默认操作是终止进程。
当应用程序对内存的引用无效时,操作系统就会向该应用程序发送该信号。该信号的系统默认操作是终止进程。
与SIGUSR1信号相同。
涉及到管道和socket,当进程向已经关闭的管道、FIFO或套接字写入信息时,那么系统将发送该信号给进程。该信号的系统默认操作是终止进程。
与系统调用alarm()或setitimer()有关,应用程序中可以调用alarm()或setitimer()函数来设置一个定时器,当定时器定时时间到,那么内核将会发送SIGALRM信号给该应用程序。该信号的系统默认操作是终止进程。
这是用于终止进程的标准信号,也是 kill 命令所发送的默认信号(kill xxx,xxx表示进程 pid),有时会直接使用"kill -9 xxx"显式向进程发送SIGKILL信号来终止进程,然而这一做法通常是错误的,这种方式应该作为最后手段,应首先尝试使用SIGTERM,实在不行再使用最后手段SIGKILL。
当父进程的某一个子进程终止时,内核会向父进程发送该信号。当父进程的某一个子进程因收到信号而停止或恢复时,内核也可能向父进程发送该信号。该信号的系统默认操作是忽略此信号,如果父进程希望被告知其子进程的这种状态改变,则应捕获此信号。
与SIGCHLD信号同义。
将该信号发送给已停止的进程,进程将会恢复运行。当进程接收到此信号时并不处于停止状态,系统默认操作是忽略该信号,但如果进程处于停止状态,则系统默认操作是使该进程继续运行。
这是一个“必停”信号,用于停止进程,应用程序无法将该信号忽略或者捕获,故而总能停止进程。
这也是一个停止信号,当用户在终端按下停止字符(通常是CTRL + Z),那么系统会将SIGTSTP信号发送给前台进程组中的每一个进程,使其停止运行。
当进程的CPU时间超出对应的资源限制时,内核将发送此信号给该进程。
应用程序调用setitimer()函数设置一个虚拟定时器,当定时器定时时间到时,内核将会发送该信号给进程。
在窗口环境中,当终端窗口尺寸发生变化时,系统会向前台进程组中的每一个进程发送该信号。
这两个信号同义。用于提示一个异步IO事件的发生,内核会向应用程序发送SIGIO信号。
如果进程发起的系统调用有误,那么内核将发送该信号给对应的进程。
以上总结可如下图所示:
Linux系统提供了系统调用signal()和sigaction()两个函数用于设置信号的处理方式。
signal()函数是Linux系统下设置信号处理方式最简单的接口,可将信号的处理方式设置为捕获信号、忽略信号以及系统默认操作,此函数原型如下所示:
#include <signal.h>
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);
函数参数和返回值含义如下:
signal()的测试中,可以看到如果是后台执行,那么按下中断符时系统是不会给后台进程发送SIGINT信号的。
当一个应用程序刚启动的时候(或者程序中没有调用signal()函数),通常情况下,进程对所有信号的处理方式都设置为系统默认操作。
当一个进程调用fork()创建子进程时,其子进程将会继承父进程的信号处理方式,因为子进程在开始时复制了父进程的内存映像,所以信号捕获函数的地址在子进程中是有意义的。
sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制,其函数原型如下所示:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
函数参数和返回值含义如下:
Linux系统提供了kill()系统调用,一个进程可通过kill()向另一个进程发送信号;除了kill()系统调用之外,Linux系统还提供了系统调用killpg()以及库函数raise(),也可用于实现发送信号的功能。
kill()系统调用可将信号发送给指定的进程或进程组中的每一个进程,其函数原型如下所示:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
函数参数和返回值含义如下:
pid为正就是发送到该pid的进程;pid为0就是发送到当前进程的进程组的每个进程;pid为-1就是发送到当前进程有权发送信号的每个进程;pid小于-1就是发送到-pid进程组的每个进程。
C库函数raise()函数可用于向自身发送信号,原型如下:
#include <signal.h>
int raise(int sig);
函数参数和返回值含义如下:
raise()其实等价于:kill(getpid(),sig);
alarm()函数可以设置一个定时器,当定时器定时时间到时,内核会向进程发送SIGALRM信号,其函数原型如下所示:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
函数参数和返回值:
每个进程只能设置一个alarm闹钟,且alarm闹钟不能循环触发。
pause()系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信号处理函数并从其返回时,pause()才返回,在这种情况下,pause()返回-1,并且将errno设置为EINTR。其函数原型如下所示:
#include <unistd.h>
int pause(void);
通常需要有一个能表示多个信号(一组信号)的数据类型—信号集(signalset),很多系统调用都使用到了信号集这种数据类型来作为参数传递,譬如sigaction()函数、sigprocmask()函数、sigpending()函数等。
信号集其实就是sigset_t类型数据结构,如下:
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} sigset_t;
sigemptyset()和sigfillset()用于初始化信号集。sigemptyset()初始化信号集,使其不包含任何信号;而sigfillset()函数初始化信号集,使其包含所有信号(包括所有实时信号),函数原型如下:
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
函数参数和返回值含义如下:
分别使用sigaddset()和sigdelset()函数向信号集中添加或移除一个信号,函数原型如下:
#include <signal.h>
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
函数参数和返回值含义如下:
sigismember()函数可以测试某一个信号是否在指定的信号集中,函数原型如下所示:
#include <signal.h>
int sigismember(const sigset_t *set, int signum);
函数参数和返回值含义如下:
Linux下,每个信号都有一串与之相对应的字符串描述信息,用于对该信号进行相应的描述。*这些字符串位于sys_siglist数组中,sys_siglist数组是一个char 类型的数组,数组中的每一个元素存放的是一个字符串指针,指向一个信号描述信息。
使用时需要包含<signal.h>头文件。
strsignal()函数可获取描述信息,原型如下:
#include <string.h>
char *strsignal(int sig);
调用strsignal()函数将会获取到参数sig指定的信号对应的描述信息,返回该描述信息字符串的指针。
psignal()可以在标准错误(stderr)上输出信号描述信息,其函数原型如下所示:
#include <signal.h>
void psignal(int sig, const char *s);
调用psignal()函数会将参数sig指定的信号对应的描述信息输出到标准错误,并且还允许调用者添加一些输出信息,由参数s指定。
内核为每一个进程维护了一个信号掩码(其实就是一个信号集)。当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理,那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理。
调用signal()或sigaction()函数为某信号设置处理方式,进程会自动将该信号加入信号掩码;sigaction()可额外指定一组信号,调用信号处理函数时将该组信号加入信号掩码中,信号处理函数结束后自动移除;sigprocmask()系统调用也可显式操作信号掩码。
sigprocmask()函数原型如下所示:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函数参数和返回值含义如下:
如果希望对一个信号解除阻塞后,然后调用pause()以等待之前被阻塞的信号的传递,这就需要将恢复信号掩码和pause()挂起进程封装为一个原子操作,也就是sigsuspend(),原型如下:
#include <signal.h>
int sigsuspend(const sigset_t *mask);
函数参数和返回值含义如下:
为了确定进程中处于等待状态的是哪些信号,可以使用sigpending()函数获取。
原型如下:
#include <signal.h>
int sigpending(sigset_t *set);
函数参数和返回值含义如下:
等待信号集只能表明信号是否发生,而无法判断发生了几次。实时信号就有如下优势:
应用程序中使用实时信号需要满足:
sigqueue()原型如下:
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
函数参数和返回值含义如下:
异常退出程序,一般使用abort()库函数,使用abort()终止进程运行,会生成核心转储文件,可用于判断程序调用abort()时的程序状态。原型如下:
#include <stdlib.h>
void abort(void);
函数abort()通常产生SIGABRT信号来终止调用该函数的进程,SIGABRT信号的系统默认操作是终止进程运行、并生成核心转储文件;当调用abort()函数之后,内核会向进程发送SIGABRT信号。
操作系统下的应用程序在运行main()函数之前需要先执行一段引导代码,最终由这段引导代码去调用应用程序的main()函数,在编译链接时,由链接器将引导代码链接到我们的应用程序当中,一起构成最终的可执行文件。程序运行需要通过操作系统的加载器来实现,加载器是操作系统中的程序,当执行程序时,加载器负责将此应用程序加载内存中去执行。
在执行传参时,命令行参数会由shell进程逐一解析,shell进程会将参数传递给加载器,加载器加载应用程序时会将其传递给应用程序引导代码,当引导程序调用main()函数时,在由它最终传递给main()函数。
大体可分为正常终止和异常终止,正常终止包括:
异常终止包括:
注册进程终止处理函数atexit(),是个库函数,用于注册一个进程在正常终止时要调用的函数,其函数原型如下所示:
#include <stdlib.h>
int atexit(void (*function)(void));
函数参数和返回值含义如下:
进程其实就是一个可执行程序的实例。
进程是一个动态过程,而非静态文件,它是程序的一次运行过程,当应用程序被加载到内存中运行之后它就称为了一个进程,当程序运行结束后也就意味着进程终止,这就是进程的一个生命周期。
Linux系统下的每一个进程都有一个进程号(processID,简称PID),在Ubuntu下执行ps命令可查看系统中进程相关信息。进程号的作用就是用于唯一标识系统中某一个进程,在某些系统调用中,进程号可以作为传入参数、有时也可作为返回值。
可通过系统调用getpid()来获取本进程的进程号,其函数原型如下所示:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
函数返回值为pid_t类型变量,便是对应的进程号。
还可以使用getppid()系统调用获取父进程的进程号,其函数原型如下所示:
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
返回值对应的便是父进程的进程号。
每一个进程都有一组与其相关的环境变量,这些环境变量以字符串形式存储在一个字符串数组列表中,把这个数组称为环境列表。其中每个字符串都是以“名称=值(name=value)”形式定义,所以环境变量是“名称-值”的成对集合。shell终端下可以用env命令查看shell进程的所有环境变量。
使用export命令可添加或删除环境变量(export -n就可以删除)。
进程的环境变量是从其父进程中继承过来的,应用程序中通过environ变量指向环境变量的字符串数组,environ是全局变量,只需申明即可使用:
extern char **environ; // 申明外部全局变量 environ
如果只想要获取某个指定的环境变量,可以使用库函数getenv(),其函数原型如下所示:
#include <stdlib.h>
char *getenv(const char *name);
函数参数和返回值含义如下:
putenv()函数可向进程的环境变量数组中添加一个新的环境变量,或者修改一个已经存在的环境变量对应的值,其函数原型如下所示:
#include <stdlib.h>
int putenv(char *string);
函数参数和返回值含义如下:
setenv()函数可以替代putenv()函数,用于向进程的环境变量列表中添加一个新的环境变量或修改现有环境变量对应的值,其函数原型如下所示:
#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
函数参数和返回值含义如下:
unsetenv()函数可以从环境变量表中移除参数name标识的环境变量,其函数原型如下所示:
#include <stdlib.h>
int unsetenv(const char *name);
可以通过将全局变量environ赋值为NULL
来清空所有变量:
environ = NULL;
也可通过clearenv()函数来操作,函数原型如下所示:
#include <stdlib.h>
int clearenv(void);
环境变量常见的用途之一是在shell中,每一个环境变量都有它所表示的含义,譬如HOME环境变量表示用户的家目录,USER环境变量表示当前用户名,SHELL环境变量表示 shell 解析器名称,PWD环境变量表示当前所在目录等,在自己的应用程序当中,也可以使用进程的环境变量。
内存的典型布局方式如下所示:
在Linux系统中,每一个进程都在自己独立的
地址空间中运行,在32位系统中,每个进程的逻辑地址空间均为4GB,这4GB的内存空间按照3:1的比例进行分配,其中用户进程享有3G的空间,而内核独自享有剩下的1G空间。
虚拟地址会通过硬件MMU(内存管理单元)映射到实际的物理地址空间中,建立虚拟地址到物理地址的映射关系后,对虚拟地址的读写操作实际上就是对物理地址的读写操作,MMU会将物理地址“翻译”为对应的物理地址。
程序访问存储器所使用的逻辑地址就是虚拟地址,通过逻辑地址映射到真正的物理内存上。所有应用程序运行在自己的虚拟地址空间中,使得进程的虚拟地址空间和物理地址空间隔离开来。
一个现有的进程可以调用系统调用fork()函数创建一个新的进程,调用fork()函数的进程称为父进程,由fork()函数创建出来的进程被称为子进程(child process),fork()函数原型如下所示(fork()为系统调用):
#include <unistd.h>
pid_t fork(void);
在一个大型的应用程序任务中,创建子进程通常会简化应用程序的设计,同时提高了系统
的并发性。
fork()调用成功后,将会在父进程中返回子进程的PID,而在子进程中返回值是0;如果调用失败,父进程返回值-1,不创建子进程,并设置errno。子进程是父进程的一个副本,每个进程均可修改各自的栈数据以及堆段中的变量,而并不影响另一个进程。
调用了fork()之后,父、子进程中一般只有一个会通过调用exit()退出进程,而另一个则应使用_exit()退出。
父进程、子进程会各自继续执行fork()之后的指令,它们共享代码段,但并不共享数据段、堆、栈等,而是子进程拥有父进程数据段、堆、栈等副本。
子进程拷贝了父进程的文件描述符表,使得父、子进程中对应的文件描述符指向了相同的文件表,也意味着父、子进程中对应的文件描述符指向了磁盘中相同的文件,因而这些文件在父、子进程间实现了共享。
父进程open打开后fork()创建子进程,子进程继承了文件描述符fd,不管是父进程,还是子进程,在每次写入时都是从文件的末尾写入,很像使用了O_APPEND标志的效果。
父进程调用fork()后,父、子进程分别open打开文件。这种文件共享方式实现的是一种两个进程分别各自对文件进行写入操作,因为父、子进程的这两个文件描述符分别指向的是不同的文件表,意味着它们有各自的文件偏移量,一个进程修改了文件偏移量并不会影响另一个进程的文件偏移量,所以写入的数据会出现覆盖的情况。
fork()应用场景:父进程希望子进程复制自己,使父进程和子进程同时执行不同的代码段;一个进程要执行不同的程序。
Linux系统还提供了vfork()系统调用用于创建子进程,vfork()与fork()函数在功能上是相同的,并且返回值也相同,在一些细节上存在区别,vfork()函数原型如下所示:
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
vfork()效率要高于fork()函数,vfork()可以为调用该函数的进程创建一个新的子进程,然而,vfork()是为子进程立即执行exec()新的程序而专门设计的。
vfork()可能会导致一些难以察觉的程序bug,所以尽量避免使用vfork()来创建子进程,除非对速度要求很高,否则使用fork()就可以了。
调用fork之后,无法确定父、子两个进程谁将率先访问CPU,也就是说无法确认谁先被系统调用运行(在多核处理器中,它们可能会同时各自访问一个CPU),这将导致谁先运行、谁后运行这个顺序是不确定的。
此时就可以通过同步技术,比如信号,例如需要让子进程先运行,则可使父进程阻塞,等待子进程来唤醒。
Linux系统下的所有进程都是由其父进程创建而来,可通过“ps -aux”命令查看系统下所有进程。PID为1的就是所有进程的父进程,通常为init进程,是Linux系统启动之后运行的第一个进程,它管理着系统上所有其它进程,init进程是由内核启动,因此理论上说它没有父进程。
通常,进程有两种终止方式:异常终止和正常终止。
_exit()函数和exit()函数的status参数定义了进程的终止状态(termination status),父进程可以调用wait()函数以获取该状态。虽然参数status定义为int类型,但仅有低8位表示它的终止状态,一般来说,终止状态为0表示进程成功终止,而非0值则表示进程在执行过程中出现了一些错误而终止。
一般都会用库函数exit()来终止:
如果采用exit(),可能会导致重复输出,解决方法如下:
父进程需要知道子进程于何时被终止,并且需要知道子进程的终止状态信息,是正常终止、还是异常终止亦或者被信号终止等,意味着父进程会对子进程进行监视。
系统调用wait()可以等待进程的任一子进程终止,同时获取子进程的终止状态信息,其函数原型如下所示:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
函数参数和返回值含义如下:
wait()有很多限制:
waitpid()系统调用则可以突破这些限制,原型如下:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
函数参数和返回值含义如下:
option是一个位掩码,可通过标志位来组合多种情况。
waitid()系统调用,与wait()和waitpid()类似,但是有更多扩展功能,这里不再赘述,可以自行通过man手册查询。
孤儿进程
父进程先于子进程结束,在Linux系统当中,所有的孤儿进程都自动成为init进程(进程号为1)的子进程。当然如果是在Ubuntu中测试,由于系统图像化界面,所以父进程会变成/sbin/upstart进程,其为后台守护进程专门收养孤儿进程。
僵尸进程
进程结束之后,通常需要其父进程回收子进程占用的一些内存资源,父进程通过调用wait()(或其变体 waitpid()、waitid()等)函数回收子进程资源,归还给系统。当子进程结束而父进程尚未回收时,子进程就会称为僵尸进程,当父进程调用wait()僵尸进程就会被内核删除;如果父进程直接退出了,此时init()进程会接管此子进程并自动调用wait()。
这里要注意,僵尸进程无法用SIGKILL将其杀死,只能通过杀死僵尸进程的父进程,使得init进程接管僵尸进程进而wait()删除。
以下两种情况会使得父进程接收该信号:
子进程的终止属于异步事件,所以可以通过这个信号来让父进程捕获子进程终止并处理回收。为了防止两个子进程相继终止,而调用信号处理函数一次只能处理一个SIGCHLD信号,在这里信号处理函数中的循环以非阻塞方式来调用waitpid(),直到没有其他终止的子进程,代码如下:
while (waitpid(-1, NULL, WNOHANG) > 0)
continue;
当子进程的工作不再是运行父进程的代码段,而是运行另一个新程序的代码,那么这个时候子进程可以通过exec函数来实现运行另一个新的程序。
系统调用execve()可以将新程序加载到某一进程的内存空间,通过调用execve()函数将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的main()函数开始执行。原型如下:
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
函数参数和返回值含义如下:
这些库函数都是基于系统调用execve()而实现的,虽然参数各异、但功能相同,包括:execl()、execlp()、execle()、execv()、execvp()、execvpe(),它们的函数原型如下所示:
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
他们之间的区别如下:
使用system()函数可以很方便地程序当中执行任意shell命令,原型如下:
#include <stdlib.h>
int system(const char *command);
函数参数和返回值含义如下:
system()函数其内部的是通过调用fork()、execl()以及waitpid()这三个函数来实现它的功能,首先system()会调用fork()创建一个子进程来运行shell(可以把这个子进程成为shell进程),并通过shell执行参数command所指定的命令。
返回值如下:
Linux系统下进程通常存在6种不同的状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(浅度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态。
各状态切换关系总结如下:
主要包括:无关系(相互独立)、父子进程关系、进程组以及会话。
1、无关系
2、父子进程关系
例如一个进程fork()之后,就是父子进程关系。
3、进程组
每个进程除了有一个进程ID、父进程ID之外,还有一个进程组ID,用于标识该进程属于哪一个进程组,进程组是一个或多个进程的集合,这些进程并不是孤立的,它们彼此之间或者存在父子、兄弟关系,或者在功能上有联系。
通过系统调用getpgrp()或getpgid()可以获取进程对应的进程组ID,其函数原型如下所示:
#include <unistd.h>
pid_t getpgid(pid_t pid);
pid_t getpgrp(void);
getpgrp()没有参数,返回值总是调用者进程对应的进程组ID;而对于getpgid()函数来说,可通过参数pid指定获取对应进程的进程组 ID,如果参数pid为0表示获取调用者进程的进程组ID。getpgid()函数成功将返回进程组ID;失败将返回-1、并设置errno。
调用系统调用setpgid()或setpgrp()可以加入一个现有的进程组或创建一个新的进程组,其函数原型如下所示:
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
int setpgrp(void);
setpgid()函数将参数pid指定的进程的进程组ID设置为参数gpid。如果这两个参数相等(pid==gpid),则由pid指定的进程变成为进程组的组长进程,创建了一个新的进程;如果参数pid等于0,则使用调用者的进程ID;另外,如果参数gpid等于0,则创建一个新的进程组,由参数pid指定的进程作为进程组组长进程。
4、会话
一个会话可包含一个或多个进程组,但只能有一个前台进程组,其它的是后台进程组;每个会话都有一个会话首领(leader),即创建会话的进程。一个会话可以有控制终端、也可没有控制终端,在有控制终端的情况下也只能连接一个控制终端,这通常是登录到其上的终端设备(在终端登录情况下)或伪终端设备(譬如通过SSH协议网络登录),一个会话中的进程组可被分为一个前台进程组以及一个或多个后台进程组。
会话的首领进程连接一个终端之后,该终端就成为会话的控制终端,与控制终端建立连接的会话首领进程被称为控制进程;产生在终端上的输入和信号将发送给会话的前台进程组中的所有进程。在Linux系统下打开了多个终端窗口时,实际上就是创建了多个终端会话。
会话的首领进程的进程组ID将作为该会话的标识,也就是会话ID(sid),在默认情况下,新创建的进程会继承父进程的会话ID。通过系统调用getsid()可以获取进程的会话ID,其函数原型如下所示:
#include <unistd.h>
pid_t getsid(pid_t pid);
使用系统调用setsid()可以创建一个会话,其函数原型如下所示:
#include <unistd.h>
pid_t setsid(void);
如果调用者进程不是进程组的组长进程,调用setsid()将创建一个新的会话,调用者进程是新会话的首领进程,同样也是一个新的进程组的组长进程,调用setsid()创建的会话将没有控制终端。setsid()调用成功将返回新会话的会话ID;失败将返回-1,并设置errno。
守护进程(Daemon)也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生,主要表现为以下两个特点:长期运行以及与控制终端脱离。
守护进程是一种很有用的进程。Linux中大多数服务器就是用守护进程实现的,譬如,Internet服务器inetd、Web服务器httpd等。同时,守护进程完成许多系统任务,譬如作业规划进程crond等。
守护进程Daemon,通常简称为d,一般进程名后面带有d就表示它是一个守护进程,用户的登录与注销与守护进程无关、不受其影响,守护进程自成进程组、自成会话,即pid=gid=sid。可用“ps -ajx”查看系统所有进程。
1、创建子进程、终止父进程
父进程调用fork()创建子进程,然后父进程使用exit()退出。
2、子进程调用setsid创建会话
关键步骤,setsid函数能够使子进程完全独立出来,从而脱离所有其他进程的控制。
3、工作目录改为根目录
4、重设文件权限掩码umask
设置文件权限掩码的函数是umask,通常的使用方法为umask(0)。
5、关闭不需要的文件描述符
6、将文件描述符0、1、2定位到/dev/null
7、其他:忽略SIGCHLD信号
当用户准备退出会话时,系统向该会话发出SIGHUP信号,会话将SIGHUP信号发送给所有子进程,子进程接收到SIGHUP信号后,便会自动终止,当所有会话中的所有进程都退出时,会话也就终止了。
当程序当中忽略SIGHUP信号之后,进程不会随着终端退出而退出。
程序只能被执行一次,只要该程序没有结束,就无法再次运行,把这种情况称为单例模式运行。
这种方法比较好想到,但是对于程序异常终止,以及删除特定的文件无法正常运行。
通过一个特定的文件来实现,当程序启动之后,首先打开该文件,调用open时一般使用O_WRONLY | O_CREAT标志,当文件不存在则创建该文件,然后尝试去获取文件锁,若是成功,则将程序的进程号(PID)写入到该文件中,写入后不要关闭文件或解锁(释放文件锁),保证进程一直持有该文件锁;若是程序获取锁失败,代表程序已经被运行、则退出本次启动。程序退出或关闭文件,文件锁自动解锁。
通过系统调用flock()、fcntl()或库函数lockf()均可实现对文件进行上锁。
这种机制在一些程序尤其是服务器程序中很常见,服务器程序使用这种方法来保证程序的单例模式运行;在Linux系统中/var/run/目录下有很多以.pid为后缀结尾的文件,这个实际上是为了保证程序以单例模式运行而设计的,作为程序实现单例模式运行所需的特定文件。
不同的进程都在各自的地址空间中、相互独立、隔离,所以它们是处在于不同的地址空间中,因此相互通信比较难,Linux内核提供了多种进程间通信的机制。
进程间通信(interprocess communication,简称IPC)指两个进程之间的通信。对于一些复杂、大型的应用程序,则会根据实际需要将其设计成多进程程序,譬如GUI、服务区应用程序等。
Linux内核提供了多种IPC机制,基本是从UNIX系统继承而来。
其中,早期的UNIX IPC包括:管道、FIFO、信号;System V IPC包括:System V信号量、System V消息队列、System V共享内存;上图中还出现了POSIX IPC,事实上,较早的System V IPC存在着一些不足之处,而POSIX IPC则是在System V IPC的基础上进行改进所形成的,弥补了System V IPC的一些不足之处。POSIX IPC包括:POSIX信号量、POSIX消息队列、POSIX共享内存。
本章学习Linux应用编程中非常重要的编程技巧——线程(Thread);与进程类似,线程是允许应用程序并发执行多个任务的一种机制,线程参与系统调度,事实上,系统调度的最小单元是线程、而并非进程。
线程是参与系统调度的最小单位。它被包含在进程之中,是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流),一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。
当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread),因为它是程序一开始时就运行的线程。应用程序都是以main()做为入口开始运行的,所以 main()函数就是主线程的入口函数,main()函数所执行的任务就是主线程需要执行的任务。
线程是程序最基本的运行单位,而进程不能运行,真正运行的是进程中的线程。同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack,称为线程栈),自己的寄存器环境(register context)、自己的线程本地存储(thread-local storage)。
在多线程应用程序中,通常一个进程中包括了多个线程,每个线程都可以参与系统调度、被CPU执行:线程是包含在进程中的,不会单独存在;线程是参与系统调度的基本单位;可并发执行;共享进程资源。
相对于多进程,多线程能够有如下优势:同一进程的多个线程切换开销较小;同一进程的多个线程通信容易;线程创建速度远大于进程创建速度;多线程在多核处理器上更有优势!
并行与串行则截然不同,并行指的是可以并排/并列执行多个任务,这样的系统,它通常有多个执行单元,所以可以实现并行运行。并行运行并不一定要同时开始运行、同时结束运行,只需满足在某一个时间段上存在多个任务被多个执行单元同时在运行着。
并发强调的是一种时分复用,与串行的区别在于,它不必等待上一个任务完成之后在做下一个任务,可以打断当前执行的任务切换执行下一个任何,这就是时分复用。在同一个执行单元上,将时间分解成不同的片段(时间片),每个任务执行一段时间,时间一到则切换执行下一个任务,依次这样轮询(交叉/交替执行),这就是并发运行。
需要注意的是,并行运行情况下的多个执行单元,每一个执行单元同样也可以以并发方式运行。
每个线程也有其对应的标识,称为线程ID。进程ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属的进程上下文中才有意义。
而线程ID使用pthread_t数据类型来表示,一个线程可通过库函数pthread_self()来获取自己的线程ID,其函数原型如下所示:
#include <pthread.h>
pthread_t pthread_self(void);
该函数调用总是成功,返回当前线程的线程ID。
可以使用pthread_equal()函数来检查两个线程ID是否相等,其函数原型如下所示:
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
如果两个线程ID t1和t2相等,则pthread_equal()返回一个非零值;否则返回0。
在Linux系统中,使用无符号长整型(unsigned long int)来表示pthread_t数据类型。
主线程可以使用库函数pthread_create()负责创建一个新的线程,创建出来的新线程被称为主线程的子线程,其函数原型如下所示:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
函数参数和返回值含义如下:
使用时,通过gcc -o编译时,需要在最后指定链接库,-lpthread。
终止线程除了在线程start函数中执行return语句,还有以下:
如果进程中的任意线程调用exit()、_exit()或者_Exit(),那么将会导致整个进程终止,这里需要注意!pthread_exit()函数将终止调用它的线程,其函数原型如下所示:
#include <pthread.h>
void pthread_exit(void *retval);
参数retval的数据类型为void *,指定了线程的返回值、也就是线程的退出码,该返回值可由另一个线程通过调用pthread_join()来获取;参数retval所指向的内容不应分配于线程栈中。
通过调用pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码,回收线程资源;函数原型如下所示:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
函数参数和返回值含义如下:
不能以非阻塞的方式调用pthread_join(),且进程中任意线程均可调用pthread_join()函数等待另一个线程的终止。
在程序设计需求当中,需要向一个线程发送一个请求,要求它立刻退出,把这种操作称为取消线程,也就是向指定的线程发送一个请求,要求其立刻终止、退出。
通过调用pthread_cancel()库函数向一个指定的线程发送取消请求,其函数原型如下所示:
#include <pthread.h>
int pthread_cancel(pthread_t thread);
发出取消请求之后,函数pthread_cancel()立即返回,不会等待目标线程的退出。所以pthread_cancel()并不会等待线程终止,仅仅只是提出请求。
默认情况下,线程是响应其它线程发送过来的取消请求的,响应请求然后退出线程。当然,线程可以选择不被取消或者控制如何被取消,通过pthread_setcancelstate()和pthread_setcanceltype()来设置线程的取消性状态和类型。原型如下:
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
pthread_setcancelstate()函数会将调用线程的取消性状态设置为参数state中给定的值,并将线程之前的取消性状态保存在参数oldstate指向的缓冲区中,如果对之前的状态不感兴趣,Linux允许将参数oldstate设置为NULL;pthread_setcancelstate()调用成功将返回 0,失败返回非0值的错误码。
如果线程的取消性状态为PTHREAD_CANCEL_ENABLE,那么对取消请求的处理则取决于线程的取消性类型,该类型可以通过调用pthread_setcanceltype()函数来设置,它的参数type指定了需要设置的类型,而线程之前的取消性类型则会保存在参数oldtype所指向的缓冲区中,如果对之前的类型不敢兴趣,Linux下允许将参数oldtype设置为NULL。同样pthread_setcanceltype()函数调用成功将返回0,失败返回非0值的错误码。
type必须为以下二选一:
所谓取消点其实就是一系列函数,当执行到这些函数的时候,才会真正响应取消请
求,这些函数就是取消点。
可通过man手册查询取消点,命令为“man 7 pthreads”。
假设线程执行的是一个不含取消点的循环(譬如for循环、while循环),那么这时线程永远也不会响应取消请求,也就意味着除了线程自己主动退出,其它线程将无法通过向它发送取消请求而终止它。此时,可使用pthread_testcancel(),可产生一个取消点,线程即可随之终结,原型如下:
#include <pthread.h>
void pthread_testcancel(void);
默认情况下,当线程终止时,其它线程可以通过调用pthread_join()获取其返回状态、回收线程资源。如果仅需线程终止时自动回收资源并移除,可以调用pthread_detach()将指定线程进行分离,也就是分离线程,原型如下所示:
#include <pthread.h>
int pthread_detach(pthread_t thread);
参数thread指定需要分离的线程,函数pthread_detach()调用成功将返回0;失败将返回一个错误码。
一个线程也可以将自己分离,处于分离状态就不能使用pthread_join()获取终止状态,且不可逆。不过终止后,可自动回收线程资源。
当线程终止退出时,去执行这样的类似进程终止处理函数,把这个称为线程清理函数(thread cleanup handler)。
与进程不同,一个线程可以注册多个清理函数,这些清理函数记录在栈中,每个线程都可以拥有一个清理函数栈。
线程通过函数pthread_cleanup_push()和pthread_cleanup_pop()分别负责向调用线程的清理函数栈中添加和移除清理函数,函数原型如下所示:
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
第一个参数routine是一个函数指针,指向一个需要添加的清理函数,routine()函数无返回值,只有一个void *类型参数;第二个参数arg,当调用清理函数routine()时,将arg作为routine()函数的参数。
pthread_cleanup_pop()的execute参数,如果为 0,清理函数不会被调用,只是将清理函数栈中最顶层的函数移除;如果为非0,则除了将清理函数栈中最顶层的函数移除之外,还会该清理函数。
当线程执行以下动作时,清理函数栈中的清理函数才会被执行:
在Linux下,使用pthread_attr_t数据类型定义线程的所有属性。
调用pthread_create()创建线程时,参数attr设置为NULL,表示使用属性的默认值创建线程。如果不使用默认值,参数attr必须要指向一个pthread_attr_t对象,而不能使用NULL。当定义pthread_attr_t对象之后 ,需要使用pthread_attr_init()函数对该对象进行初始化操作 ,当对象不再使用时,需要使用pthread_attr_destroy()函数将其销毁,函数原型如下所示:
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
在调用成功时返回0,失败将返回一个非0值的错误码。
比较关注属性包括:线程栈的位置和大小、线程调度策略和优先级,以及线程的分离状态属性等。
pthread_attr_t数据结构中定义了栈的起始地址以及栈大小,调用函数pthread_attr_getstack()可以获取这些信息,函数pthread_attr_setstack()对栈起始地址和栈大小进行设置,其函数原型如下所示:
#include <pthread.h>
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
函数pthread_attr_getstack(),参数和返回值含义如下:
函数pthread_attr_setstack(),参数和返回值含义如下:
如果想单独获取或设置栈大小、栈起始地址,可以使用下面这些函数:
#include <pthread.h>
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);
如果在创建线程时就确定要将该线程分离,可以修改pthread_attr_t结构中的detachstate线程属性,让线程一开始运行就处于分离状态。调用函数pthread_attr_setdetachstate()设置detachstate线程属性,调用pthread_attr_getdetachstate()获取detachstate线程属性,其函数原型如下所示:
#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
参数attr指向pthread_attr_t对象;调用pthread_attr_setdetachstate()函数将detachstate线程属性设置为参数detachstate所指定的值。函数pthread_attr_getdetachstate()用于获取 detachstate 线程属性,将detachstate线程属性保存在参数detachstate所指定的内存中。
每个线程都有自己的栈地址空间,那么每个线程运行过程中所定义的自动变量(局部变量)都是分配在自己的线程栈中的,它们不会相互干扰。
如果一个函数被同一进程的多个不同的执行流同时调用,每次函数调用总是能产生正确的结果(或者叫产生预期的结果),把这样的函数就称为可重入函数。
在多线程环境以及信号处理有关应用程序中,需要注意不可重入函数的问题,如果多条执行流同时调用一个不可重入函数则可能会得不到预期的结果、甚至有可能导致程序崩溃!不止是在应用程序中,在一个包含了中断处理的裸机应用程序中亦是如此!所以不可重入函数通常存在着一定的安全隐患。
很多的C库函数有两个版本:可重入版本和不可重入版本,可重入版本函数其名称后面加上了“_r”,用于表明该函数是一个可重入函数。可通过man手册查询。
一个函数被多个线程(其实也是多个执行流,但是不包括由信号处理函数所产生的执行流)同时调用时,它总会一直产生正确的结果,把这样的函数称为线程安全函数。线程安全函数包括可重入函数,可重入函数是线程安全函数的真子集,即可重入函数一定是线程安全函数。
判断一个函数是否为线程安全函数的方法是,该函数被多个线程同时调用是否总能产生正确的结果,如果每次都能产生预期的结果则表示该函数是一个线程安全函数。
在多线程编程环境下,有些代码段只需要执行一次,就可使用pthread_once()实现,原型如下:
#include <pthread.h>
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
函数参数和返回值含义如下:
线程特有数据也称为线程私有数据,简单点说,就是为每个调用线程分别维护一份变量的副本(copy),每个线程通过特有数据键(key)访问时,这个特有数据键都会获取到本线程绑定的变量副本。这样就可以避免变量成为多个线程间的共享数据。
线程特有数据的核心思想,就是为每一个调用线程(调用某函数的线程,该函数就是要通过线程特有数据将其实现为线程安全的函数)分配属于该线程的私有数据区,为每个调用线程分别维护一份变量的副本。
在为线程分配私有数据区之前,需要调用pthread_key_create()函数创建一个特有数据键(key),并且只需要在首个调用的线程中创建一次即可,所以通常会使用到pthread_once()函数。pthread_key_create()函数原型如下所示:
#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
函数参数和返回值含义如下:
调用pthread_key_create()函数创建特有数据键(key)后通常需要为调用线程分配私有数据缓冲区,譬如通过malloc()申请堆内存,每个调用线程分配一次,且只会在线程初次调用此函数时分配。为线程分配私有数据缓冲区之后,通常需要调用pthread_setspecific()函数,pthread_setspecific()函数其实完成
了这样的操作:首先保存指向线程私有数据缓冲区的指针,并将其与特有数据键以及当前调用线程关联起来;其函数原型如下所示:
#include <pthread.h>
int pthread_setspecific(pthread_key_t key, const void *value);
函数参数和返回值含义如下:
调用pthread_setspecific()函数将线程私有数据缓冲区与调用线程以及特有数据键关联之后,便可以使用pthread_getspecific()函数来获取调用线程的私有数据区了。其函数原型如下所示:
#include <pthread.h>
void *pthread_getspecific(pthread_key_t key);
参数key应赋值为调用pthread_key_create()函数时创建的特有数据键,也就是pthread_key_create()函数的参数key指向的pthread_key_t变量。
如果需要删除一个特有数据键(key)可以使用函数 thread_key_delete(),
thread_key_delete()函数删除先前由pthread_key_create()创建的键。其函数原型如下所示:
#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
参数key为要删除的键。函数调用成功返回0,失败将返回一个错误编号。
通常在调用pthread_key_delete()之
前,必须确保以下条件:
通常情况下,程序中定义的全局变量是进程中所有线程共享的,所有线程都可以访问这些全局变量;而线程局部存储在定义全局或静态变量时,使用__thread修饰符修饰变量,此时,每个线程都会拥有一份对该变量的拷贝。线程局部存储中的变量将一直存在,直至线程终止,届时会自动释放这一存储。
关于线程局部变量的声明和使用,需要注意以下几点:
主要包括线程与信号之间牵扯的问题、线程与进程控制(fork()、exec()、exit()等)之间的交互。
信号既要能够在传统的单线程进程中保持它原有的功能、特性,与此同时,又需要设计出能够适用于多线程环境的新特性!
信号模型在一些方面是属于进程层面(由进程中的所有线程线程共享)的,而在另一些方面是属于单个线程层面的,以下对其进行汇总:
在多线程环境下,使用pthread_sigmask()函数来设置各个线程的信号掩码,其函数原型如下所示:
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
pthread_sigmask()函数就像sigprocmask()一样,不同之处在于它在多线程程序中使用,所以pthread_sigmask()函数的用法与sigprocmask()完全一样。
在多线程程序中,可以通过pthread_kill()向同一进程中的某个指定线程发送信号,其函数原型如下所示:
#include <signal.h>
int pthread_kill(pthread_t thread, int sig);
参数thread,也就是线程ID,用于指定同一进程中的某个线程,调用pthread_kill()将向参数thread指定的线程发送信号sig。如果参数sig为0,则不发送信号,但仍会执行错误检查。函数调用成功返回0,失败将返回一个错误编号,不会发送信号。
pthread_sigqueue()函数执行与sigqueue类似的任务,但它不是向进程发送信号,而是向同一进程中的某个指定的线程发送信号。其函数原型如下所示:
#include <signal.h>
#include <pthread.h>
int pthread_sigqueue(pthread_t thread, int sig, const union sigval value);
参数thread为线程ID,指定接收信号的目标线程(目标线程与调用pthread_sigqueue()函数的线程是属于同一个进程),参数sig指定要发送的信号,参数value指定伴随数据,与sigqueue()函数中的value参数意义相同。
异步信号安全函数指的是可以在信号处理函数中可以被安全调用的线程安全函数,所以它比线程安全函数的要求更为严格!可重入函数满足这个要求,所以可重入函数一定是异步信号安全函数。而线程安全函数则不一定是异步信号安全函数了。
这里在linux驱动的时候已经有过涉及了,这里就不过多展开,大概记一下API。
线程同步是为了对共享资源的访问进行保护。保护的目的是为了解决数据一致性的问题。出现数据一致性问题其本质在于进程中的多个线程对共享资源的并发访问(同时访问)。
互斥锁使用pthread_mutex_t数据类型表示,在使用互斥锁之前,必须首先对它进行初始化操作,可以使用两种方式对互斥锁进行初始化操作。
使用PTHREAD_MUTEX_INITIALIZER宏初始化互斥锁。只适用于在定义的时候就直接进行初始化。
可以使用pthread_mutex_init()函数对互斥锁进行初始化,其函数原型如下所示:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
函数参数和返回值含义如下:
互斥锁初始化之后,处于一个未锁定状态,调用函数 thread_mutex_lock()可以对互斥锁加锁、获取互斥锁,而调用函数pthread_mutex_unlock()可以对互斥锁解锁、释放互斥锁。其函数原型如下所示:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数mutex指向互斥锁对象;pthread_mutex_lock()和pthread_mutex_unlock()在调用成功时返回0;失败将返回一个非0值的错误码。
当互斥锁已经被其它线程锁住时,调用pthread_mutex_lock()函数会被阻塞,直到互斥锁解锁;如果线程不希望被阻塞,可以使用pthread_mutex_trylock()函数;调用pthread_mutex_trylock()函数尝试对互斥锁进行加锁,如果互斥锁处于未锁住状态,那么调用pthread_mutex_trylock()将会锁住互斥锁并立马返回,如果互斥锁已经被其它线程锁住,调用pthread_mutex_trylock()加锁失败,但不会阻塞,而是返回错误码EBUSY。原型如下:
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数mutex指向目标互斥锁,成功返回0,失败返回一个非0值的错误码,如果目标互斥锁已经被其它线程锁住,则调用失败返回EBUSY。
当不再需要互斥锁时,应该将其销毁,通过调用pthread_mutex_destroy()函数来销毁互斥锁,其函数原型如下所示:
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数mutex指向目标互斥锁;同样在调用成功情况下返回0,失败返回一个非0值的错误码。
当超过一个线程对同一组互斥锁(两个或两个以上的互斥锁)进行加锁时,就有可能发生死锁。两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,会被一直阻塞。
调用pthread_mutex_init()函数初始化互斥锁时可以设置互斥锁的属性,通过参数attr指定。如果不使用默认属性,在调用pthread_mutex_init()函数时,参数attr必须要指向一个pthread_mutexattr_t对象,而不能使用NULL。当定义pthread_mutexattr_t对象之后,需要使用pthread_mutexattr_init()函数对该对象进行初始化操作,当对象不再使用时,需要使用pthread_mutexattr_destroy()将其销毁,函数原型如下所示:
#include <pthread.h>
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
参数attr指向需要进行初始化的pthread_mutexattr_t对象,调用成功返回0,失败将返回非0值的错误码。
可以使用pthread_mutexattr_gettype()函数得到互斥锁的类型属性,使用pthread_mutexattr_settype()修改/设置互斥锁类型属性,其函数原型如下所示:
#include <pthread.h>
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
参数attr指向pthread_mutexattr_t类型对象;对于pthread_mutexattr_gettype()函数,函数调用成功会将互斥锁类型属性保存在参数type所指向的内存中,通过它返回出来;而对于pthread_mutexattr_settype()函数,会将参数attr指向的pthread_mutexattr_t对象的类型属性设置为参数type指定的类型。
条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,直到某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。使用条件变量主要包括两个动作:
条件变量允许一个线程休眠(阻塞等待)直至获取到另一个线程的通知(收到信号)再去执行自己的操作。
条件变量使用pthread_cond_t数据类型来表示,类似于互斥锁,在使用条件变量之前必须对其进行初始化。初始化方式同样也有两种:使用宏PTHREAD_COND_INITIALIZER或者使用函数pthread_cond_init(),使用宏的初始化方法与互斥锁的初始化宏一样。
pthread_cond_init()函数原型如下所示:
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
使用pthread_cond_init()函数初始化条件变量,当不再使用时,使用pthread_cond_destroy()销毁条件变量。参数 cond 指向 pthread_cond_t 条件变量对象,对于pthread_cond_init()函数,类似于互斥锁,在初始化条件变量时设置条件变量的属性,参数attr指向一个pthread_condattr_t类型对象pthread_condattr_t数据类型用于描述条件变量的属性。可将参数attr设置为NULL,表示使用属性的默认值来初始化条件变量,与使用PTHREAD_COND_INITIALIZER宏相同。函数调用成功返回0,失败将返回一个非0值的错误码。
发送信号操作即是通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变,这些处于等待状态的线程收到通知之后便会被唤醒,唤醒之后再检查条件是否满足。等待操作是指在收到一个通知前一直处于阻塞状态。
函数pthread_cond_signal()和pthread_cond_broadcast()均可向指定的条件变量发送信号,通知一个或多个处于等待状态的线程。调用pthread_cond_wait()函数是线程阻塞,直到收到条件变量的通知。
pthread_cond_signal()和pthread_cond_broadcast()函数原型如下所示:
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
参数cond指向目标条件变量,向该条件变量发送信号。调用成功返回0;失败将返回一个非0值的错误码。
两者区别在于:对阻塞于pthread_cond_wait()的多个线程对应的处理方式不同,pthread_cond_signal()函数至少能唤醒一个线程,而pthread_cond_broadcast()函数则能唤醒所有线程。使用pthread_cond_broadcast()函数总能产生正确的结果,唤醒所有等待状态的线程,但函数pthread_cond_signal()会更为高效,因为它只需确保至少唤醒一个线程即可。
pthread_cond_wait()函数原型如下所示:
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
函数参数与返回值意义如下:
在pthread_cond_wait()函数内部会对参数mutex所指定的互斥锁进行操作,通常情况下,条件判断以及pthread_cond_wait()函数调用均在互斥锁的保护下,也就是说,在此之前线程已经对互斥锁加锁了。条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制。
使用条件变量,都会有与之相关的判断条件,通常情况下,会涉及到一个或多个共享变量。必须使用while循环,而不是if语句,这是一种通用的设计原则:当线程从pthread_cond_wait()返回时,并不能确定判断条件的状态,应该立即重新检查判断条件,如果条件不满足,那就继续休眠等待。
调用pthread_cond_init()函数初始化条件变量时,可以设置条件变量的属性,通过参数attr指定。参数attr指向一个pthread_condattr_t类型对象,该对象对条件变量的属性进行定义,当然,如果将参数attr设置为NULL,表示使用默认值来初始化条件变量属性。条件变量包括两个属性:进程共享属性和时钟属性。
如果在获取自旋锁时,自旋锁处于未锁定状态,那么将立即获得锁(对自旋锁上锁);如果在获取自旋锁时,自旋锁已经处于锁定状态了,那么获取锁操作将会在原地“自旋”,直到该自旋锁的持有者释放了锁。
自旋锁一直占用的CPU,它在未获得锁的情况下,一直处于运行状态(自旋),所以占着CPU,如果不能在很短的时间内获取锁,这无疑会使CPU效率降低。
自旋锁通常用于以下情况:需要保护的代码段执行时间很短,这样就会使得持有锁的线程会很快释放锁,而“自旋”等待的线程也只需等待很短的时间;在这种情况下就比较适合使用自旋锁,效率高!
自旋锁使用pthread_spinlock_t数据类型表示,当定义自旋锁后,需要使用pthread_spin_init()函数对其进行初始化,当不再使用自旋锁时,调用pthread_spin_destroy()函数将其销毁,其函数原型如下所示:
#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
参数lock指向了需要进行初始化或销毁的自旋锁对象,参数pshared表示自旋锁的进程共享属性。这两个函数在调用成功的情况下返回0;失败将返回一个非0值的错误码。
可以使用pthread_spin_lock()函数或pthread_spin_trylock()函数对自旋锁进行加锁,前者在未获取到锁时一直“自旋”;对于后者,如果未能获取到锁,就立刻返回错误,错误码为EBUSY。不管以何种方式加锁,
自旋锁都可以使用pthread_spin_unlock()函数对自旋锁进行解锁。其函数原型如下所示:
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
参数lock指向自旋锁对象,调用成功返回0,失败将返回一个非0值的错误码。
读写锁有3种状态:读模式下的加锁状态(简称读加锁状态)、写模式下的加锁状态(以下简称写加锁状态)和不加锁状态(见),一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。因此可知,读写锁比互斥锁具有更高的并行性!
读写锁有如下两个规则:
读写锁非常适合于对共享数据读的次数远大于写的次数的情况。读写锁也叫做共享互斥锁。当读写锁是读模式锁住时,就可以说成是共享模式锁住。当它是写模式锁住时,就可以说成是互斥模式锁住。
与互斥锁、自旋锁类似,在使用读写锁之前也必须对读写锁进行初始化操作,读写锁使用pthread_rwlock_t数据类型表示,读写锁的初始化可以使用宏PTHREAD_RWLOCK_INITIALIZER或者函数pthread_rwlock_init(),其初始化方式与互斥锁相同。
可以使用pthread_rwlock_init()函数对其进行初始化,当读写锁不再使用时,需要调用pthread_rwlock_destroy()函数将其销毁,其函数原型如下所示:
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
参数rwlock指向需要进行初始化或销毁的读写锁对象。对于pthread_rwlock_init()函数,参数attr是一
个pthread_rwlockattr_t *类型指针,指向pthread_rwlockattr_t对象。pthread_rwlockattr_t 数据类型定义了读写锁的属性,若将参数attr设置为NULL,则表示将读写锁的属性设置为默认值,在这种情况下其实就等价于PTHREAD_RWLOCK_INITIALIZER这种方式初始化,而不同之处在于,使用宏不进行错误检查。调用成功返回0,失败将返回一个非0值的错误码。
当读写锁不再使用时,需要调用pthread_rwlock_destroy()函数将其销毁。
以读模式对读写锁进行上锁,需要调用pthread_rwlock_rdlock()函数;以写模式对读写锁进行上锁,需要调用pthread_rwlock_wrlock()函数。不管是以何种方式锁住读写锁,均可以调用pthread_rwlock_unlock()函数解锁,其函数原型如下所示:
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
参数rwlock指向读写锁对象。调用成功返回0,失败返回一个非0值的错误码。
当读写锁处于写模式加锁状态时,其它线程调用pthread_rwlock_rdlock()或pthread_rwlock_wrlock()函数均会获取锁失败,从而陷入阻塞等待状态;当读写锁处于读模式加锁状态时,其它线程调用pthread_rwlock_rdlock()函数可以成功获取到锁,如果调用pthread_rwlock_wrlock()函数则不能获取到锁,从而陷入阻塞等待状态。
如果线程不希望被阻塞,可以调用pthread_rwlock_tryrdlock()和pthread_rwlock_trywrlock()来尝试加锁。
如果不可以获取锁时,这两个函数都会立马返回错误,错误码为EBUSY。其函数原型如下所示:
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
读写锁的属性使用pthread_rwlockattr_t数据类型来表示,当定义pthread_rwlockattr_t对象时,需要使用pthread_rwlockattr_init()函数对其进行初始化操作,初始化会将pthread_rwlockattr_t对象定义的各个读写锁属性初始化为默认值;当不再使用pthread_rwlockattr_t对象时,需要调用pthread_rwlockattr_destroy()函数将其销毁,其函数原型如下所示:
#include <pthread.h>
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
参数attr指向需要进行初始化或销毁的pthread_rwlockattr_t对象;函数调用成功返回0,失败将返回一个非0值的错误码。
读写锁只有一个属性,那便是进程共享属性。Linux下提供了相应的函数用于设置或获取读写锁的共享属性。函数pthread_rwlockattr_getpshared()用于从pthread_rwlockattr_t对象中获取共享属性,函数pthread_rwlockattr_setpshared()用于设置pthread_rwlockattr_t对象中的共享属性,其函数原型如下所示:
#include <pthread.h>
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared)
函数pthread_rwlockattr_getpshared()参数和返回值:
函数pthread_rwlockattr_setpshared()参数和返回值:
本章介绍了线程同步的几种不同的方法,包括互斥锁、条件变量、自旋锁以及读写锁。在实际应用开发当中,用的最多的还是互斥锁和条件变量。
主要包括:非阻塞I/O、I/O多路复用、异步I/O、存储映射I/O以及文件锁。
阻塞其实就是进入了休眠状态,交出了CPU控制权。非阻塞式I/O,即使没有数据可读,也不会被阻塞、而是会立马返回错误!
在调用open()函数打开文件时,为参数flags指定O_NONBLOCK标志,open()调用成功后,后续的I/O操作将以非阻塞式方式进行。
这里可通过读取鼠标信息来学习两者区别。鼠标是输入设备,在/dev/input中,可通过“ls -lh”查看,并通过"sudo od -x /dev/input/eventX"来确定具体的文件设备。
如果采用非阻塞IO,那么open的时候要加上O_NONBLOCK,并且read需要在一个死循环中,从而完成轮询读取。
阻塞式I/O的优点在于能够提升CPU的处理效率,当自身条件不满足时,进入阻塞状态,交出CPU资源,将CPU资源让给别人使用;而非阻塞式则是抓紧利用CPU资源,譬如不断地去轮询,这样就会导致该程序占用了非常高的CPU使用率!
如果是读取键盘输入,他的标准输入设备是stdin,进程会自动继承,标准输入设备的fd为0,可直接使用而不用open。
阻塞就会导致无法并发读取数据(同时读取),当然可以通过多线程或父子进程来完成,但通过非阻塞IO会更简单。当然,此时就会因为轮询导致CPU占用率高。
I/O多路复用(IO multiplexing)通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也就是某个文件)可以执行I/O操作时,能够通知应用程序进行相应的读写操作。I/O多路复用技术是为了解决:在并发式I/O场景中进程或线程阻塞到某个I/O系统调用而出现的技术,使进程不阻塞于某个特定的I/O系统调用。
可以采用两个功能几乎相同的系统调用来执行I/O多路复用操作,分别是系统调用select()和poll()。
I/O多路复用存在一个非常明显的特征:外部阻塞式,内部监视多路I/O。
系统调用select()可用于执行I/O多路复用操作,调用select()会一直阻塞,直到某一个或多个文件描述符成为就绪态(可以读或写)。其函数原型如下所示:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
其中参数readfds、writefds以及exceptfds都是fd_set类型指针,指向一个fd_set类型对象,fd_set数据类型是一个文件描述符的集合体,所以参数readfds、writefds以及exceptfds都是指向文件描述符集合的指针。
fd_set 数据类型是以位掩码的形式来实现的,Linux提供了四个宏用于对fd_set类型对象进行操作,FD_CLR()、FD_ISSET()、FD_SET()、FD_ZERO()。
参数nfds通常表示最大文件描述符编号值加1,考虑readfds、writefds以及exceptfds这三个文件描述符集合,在3个描述符集中找出最大描述符编号值,然后加1。
timeout可用于设定select()阻塞的时间上限,控制select的阻塞行为,可将timeout参数设置为NULL,表示select()将会一直阻塞、直到某一个或多个文件描述符成为就绪态;也可将其指向一个struct timeval结构体对象。
四个宏定义:
#include <sys/select.h>
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
文件描述符集合有一个最大容量限制,有常量FD_SETSIZE来决定,在Linux系统下,该常量的值为1024。在定义一个文件描述符集合之后,必须用FD_ZERO()宏将其进行初始化操作,然后再向集合中添加关心的各个文件描述符。
返回值:
系统调用poll()与select()函数很相似,但函数接口有所不同。需要构造一个struct pollfd类型的数组,每个数组元素指定一个文件描述符以及我们对该文件描述符所关心的条件(数据可读、可写或异常情况)。poll()函数原型如下所示:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函数参数含义如下:
struct pollfd结构体如下所示:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
fd是一个文件描述符,struct pollfd结构体中的events和revents都是位掩码,初始化events来指定需要为文件描述符fd做检查的事件。当poll()函数返回时,revents变量由poll()函数内部进行设置,用于说明文件描述符fd发生了哪些事件(poll()没有更改events变量)。
poll()返回值与select()一样。
在使用select()或poll()时需要注意一个问题,当监测到某一个或多个文件描述符成为就绪态(可以读或写)时,需要执行相应的I/O操作,以清除该状态,否则该状态将会一直存在。
在异步I/O中,当文件描述符上可以执行I/O操作时,进程可以请求内核为自己发送一个信号。之后进程就可以执行任何其它的任务直到文件描述符可以执行I/O操作为止,此时内核会发送信号给进程。
使用步骤如下:
O_ASYNC标志可用于使能文件描述符的异步I/O事件,该标志主要用于异步I/O。调用open()时无法通过指定O_ASYNC标志来使能异步I/O,但可以使用fcntl()函数添加O_ASYNC标志使能异步I/O。
通过fcntl()函数进行设置,操作命令cmd设置为F_SETOWN,第三个参数传入接收进程的进程ID(PID),通常将调用进程的PID传入。
通过signal()或sigaction()函数为SIGIO信号注册一个信号处理函数,当进程接收到内核发送过来的SIGIO信号时,会执行该处理函数,在处理函数当中执行相应的I/O操作。
在一个需要同时检查大量文件描述符的应用程序中,例如某种类型的网络服务端程序,与select()和poll()相比,异步I/O能够提供显著的性能优势。
SIGIO作为异步I/O通知的默认信号,是一个非实时信号,可以设置不使用默认信号,指定一个实时信号作为异步I/O通知信号。只要调用fcntl()函数,第3个参数arg指定实时信号编号即可。
使用sigaction函数进行注册,并为sa_flags参数指定SA_SIGINFO,表示使用sa_sigaction指向的函数作为信号处理函数,而不使用sa_handler指向的函数。
函数参数中包括一个siginfo_t指针,指向siginfo_t类型对象,当触发信号时该对象由内核构建。对于异步I/O,传递给信号处理函数的siginfo_t结构体有如下相关字段:
存储映射I/O(memory-mapped I/O)是一种基于内存区域的高级I/O操作,它能将一个文件映射到进程地址空间中的一块内存区域中,当从这段内存中读数据时,就相当于读文件中的数据(对文件进行read操作),将数据写入这段内存时,则相当于将数据直接写入文件中(对文件进行write操作)。
为了实现存储映射I/O这一功能,需要告诉内核将一个给定的文件映射到进程地址空间中的一块内存区域中,这由系统调用mmap()来实现。其函数原型如下所示:
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
函数参数和返回值含义如下:
参数addr和offset在不为NULL和0的情况下,addr和offset的值通常被要求是系统页大小的整数倍,可通过sysconf()函数获取页大小,以字节为单位(sysconf(_SC_PAGE_SIZE))。
相关信号:
通过mmap()将文件映射到进程地址空间中
的一块内存区域中,当不再需要时,必须解除映射,使用munmap()解除映射关系,其函数原型如下所示:
#include <sys/mman.h>
int munmap(void *addr, size_t length);
参数addr指定待解除映射地址范围的起始地址,它必须是系统页大小的整数倍;参数length是一个非负整数,指定了待解除映射区域的大小(字节数),被解除映射的区域对应的大小也必须是系统页大小的整数倍,length并不一定为页大小整数倍。
当进程终止时也会自动解除映射(如果程序中没有显式调用munmap()),但调用close()关闭文件时并不会解除映射。
使用系统调用mprotect()可以更改一个现有映射区的保护要求,其函数原型如下所示:
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
参数prot的取值与mmap()函数的prot参数的一样,mprotect()函数会将指定地址范围的保护要求更改为参数prot所指定的类型,参数addr指定该地址范围的起始地址,addr的值必须是系统页大小的整数倍;参数len指定该地址范围的大小。mprotect()函数调用成功返回0;失败将返回-1,并且会设置errno来只是错误原因。
可以调用msync()函数将映射区中的数据刷写、更新至磁盘文件中(同步操作),系统调用msync()类似fsync()函数,不过msync()作用于映射区。该函数原型如下所示:
#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);
参数addr和length指定了需同步的内存区域的起始地址和大小。对于参数addr来说,同样也要求必须是系统页大小的整数倍,也就是与系统页大小对齐。参数flags应指定为MS_ASYNC和MS_SYNC两个标志之一,除此之外,还可以根据需求选择是否指定MS_INVALIDATE标志,作为一个可选标志。
msync()函数在调用成功情况下返回0;失败将返回-1、并设置errno。
普通I/O方式一般是通过调用read()和write()函数来实现对文件的读写,使用read()和write()读写文件时,函数经过层层的调用后,才能够最终操作到文件,中间涉及到很多的函数调用过程,效率会比较低。
存储映射I/O的实质其实是共享,与IPC之内存共享很相似。由于源文件和目标文件都已映射到了应用层的内存区域中,所以直接操作映
射区来实现文件复制。其所映射的文件只能是固定大小,且映射的内存区域必须是系统页整数倍。使用存储映射I/O在进行大数据量操作时比较有效。
文件锁,顾名思义是一种应用于文件的锁机制,当多个进程同时操作同一文件时,为了保证数据正确性,linux通常采用的方法是对文件上锁,来避免多个进程同时操作同一文件时产生竞争状态。
文件锁可分为建议性锁和强制性锁:
系统调用flock(),使用该函数可以对文件加锁或者解锁,但是flock()函数只能产生建议性锁,其函数原型如下所示:
#include <sys/file.h>
int flock(int fd, int operation);
函数参数和返回值含义如下:
使用flock()的几个原则:
当一个文件描述符被复制时(譬如使用 dup()、dup2()或fcntl()F_DUPFD操作),这些通过复制得到的文件描述符和源文件描述符都会引用同一个文件锁,使用这些文件描述符中的任何一个进行解锁都可以。
fcntl()函数是一个多功能文件描述符管理工具箱,通过配合不同的cmd操作命令来实现不同的功能。原型如下:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );
与锁相关的cmd为F_SETLK、F_SETLKW、F_GETLK,第三个参数flockptr是一个struct flock结构体指针。
fcntl()与flock()有两个较大区别:
可以设置为F_RDLCK、F_WRLCK和F_UNLCK三种类型之一,F_RDLCK表示共享性质的读锁,F_WRLCK表示独占性质的写锁,F_UNLCK表示解锁一个区域。任意多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定的字节上只能有一个进程有一把独占写锁。
文件锁相关的三个cmd它们的作用:
使用fcntl()的几个原则:
如果复制文件,与flock()一样,随便用哪个文件描述符都可以解锁。
一般不建议使用强制性锁,所以大部分情况下使用的都是建议性锁。
如果要开启强制性锁机制,需要设置文件的 Set-Group-ID(S_ISGID)位为 1,并且禁止文件的组用户执行权限(S_IXGRP),也就是将其设置为0。但是也有些Linux是不支持强制性锁机制的。
lockf()函数是一个库函数,其内部是基于fcntl()来实现的,所以lockf()是对fcntl锁的一种封装。
这一章主要是非阻塞I/O、I/O多路复用、异步I/O、存储映射I/O以及文件锁:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。