目录
一、守护进程与系统日志
1.daemon进程
Uinx/Linux中的守护进程 (daemon)类似于Windows的后台服务进程,一直在后台长时间运行。他通常在系统启动后就开始运行,没有控制终端,也无法和前台的用户交互,在系统关闭是才结束。Daemon程序一般作为服务程序使用,等待客户端程序与他通信。我们也把运行中的Daemon程序叫做守护进程。
在我们在Linux上打开一个命令行终端,输入编译命令进行编译,编译时间有可能需要花费很长时间,在编译过程中,如果不小心关闭了当前这个terminal那么编译中断;这是因为编译脚本是作为当前终端的一个子进程来执行的,当终端退出后,子进程自然也会退出。在学完Linux基本命令之后我们知道一个命令加上“&”可以让该命令到后台去运行,例如:
make &
该命令会让make命令到后台执行,但这样只是造成了make在后台一直运行的假象,他依然 没有脱离与terminal之间的父子关系,当terminal退出后,make依然会退出。而daemon进程,可以让命令不会随着terminal的退出而停止运行,所以针对daemon进程就要用特殊的编译来处理。
Linux专门提供了一个用来创建daemon的原函数,该函数的原型是:
#include<unistd.h>
int daemon(int nochdir, int noclose)
其中参数nochdir指定是否切换工作路径到''/"根目录,参数noclose指定是否要关闭标准输入、标准输出、和标准出错(即重定向到/dev/null)。在一般实际应用中,通常这两个参数都传0.
2.daemon进程工作原理
调用两次fork()的原因:
(1)第一次调用fork()使父进程终止,让子进程进入后台运行同时保证了会话id与当前子进程id不同,可以调用setsid创建一个新的会话,保证了子进程是会话组长(sid==pid),也是进程组组长(pgid == pid)。
(2)第二次fork目的是创建一个子进程b,现在子进程b与init终端已经不在一个进程组和会话组中,完全独立开来,这就是daemon()的实现原理。
3.syslog
对于后台默默运行的守护进程,本身不应该往标准输出或标准出错上输出程序的任何状态信息 ,我们一般会关闭三个标准I/O,这样我门就不能在表面上看到程序的运行状态信息。当然我们可是自己写函数把自己程序运行的相关信息存到一个文件中,另一种方法是使用Linux自带的syslog机制。syslog是一种工业标准的协议,可以用来记录设备的日志。
下面介绍一下Linux系统自带的日志系统syslog函数:
#include<syslog.h>
void openlog(const char *ident,int option,int facility);
函数说明:
打开日志设备,以供读取和写入,与文件调用的open相似;调用openlog是可选择的,如果不调用openlog,则在第一次调用syslog时,自动调用openlog。
参数说明:
ident:是一个标记,ident所表示的字符串将固定的加在每行日志的前面标识这个日志,通常就写成当前程序名以做标记;
option:指定openlog函数和接下来调用的syslog函数的控制日志。可以取以下值:
LOG_CONS 如果将信息 发送给syslog守护进程时发生错误,直接将相关信息输出到终端;
LOG_NEDLAY 立即打开与日志的连接(通常情况下,只有产生第一条日志信息的情况下才会打开与日志系统的连接)
LOG_ODELAY 类似于LOG_NEDLAY参数,与系统日志的连接只有在syslog函数调用时才会创建
LOG_PERROR 在将信息写入日志的同时,将信息发送到标准错误输出 LOG_PID 每条日志信息中都包含进程号
LOG_AUTH 认证系统
LOG_AUTHPRIV 同LOG_AUTH只登陆到所选择的单个用户可读的文件中
.......
void syslog(int priority,const char *format,...);
函数说明:写入日志,与文件系统调用printf使用方法类似,但在前面指定日志级别。
参数说明:
priority:表示消息的级别,与syslog守护进程的配置文件syslog.conf中的level相对应,可取如下值:
LOG_EMERG: 紧急情况
LOG_ALERT: 应该被立即改正的问题,如系统数据库被破坏
LOG_CRIT: 重要情况,如硬盘错误
LOG_ERR: 错误
LOG_WARNING 警告信息
LOG_NOTICE 不是错误情况,但是可能需要处理
LOG_INFO 情报错误
LOG_DEBUG 包含情报信息,通常指在调试一个程序时使用
void closelog(void);
函数说明:关闭日志设备,与文件系统调用的close类似;调用closelog也是可选择的,他只是关闭被用于与syslog守护进程通信的描述符。
接下来创建一个C程序来演示daemon守护进程的使用:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<syslog.h>
#include<libgen.h> /* basename() */
int main(int argc,char **argv)
{
char *progname = basename(argv[0]);/*截取文件名*/
if(daemon(0,0) < 0)
{
printf("program daemon() failure: %s\n",strerror(errno));
return -1;
}
/*设置将错误信息输出到终端和让每条日志信息都包括进程号*/
openlog("daemon",LOG_CONS|LOG_PID,0);
syslog(LOG_NOTICE,"program '%s' start running\n",progname);
syslog(LOG_WARNING,"program '%s' running with a warnning message\n",progname);
syslog(LOG_EMERG,"program '%s' running with a emergency message\n",progname);
while(1)
{
;
}
syslog(LOG_NOTICE,"program '%s' stop running\n",progname);
closelog();
return 0;
}
运行结果如下:
所有程序(包括Linux内核)调用syslog()函数输出的相关信息都会记录到/var/log/messages日志文件中,但因为该日志文件中记录了所有信息,这也意味着当前程序记录的消息容易被别的程序冲刷掉,所以一般会使用标准文件IO库实现自己的日志系统,而不是直接调用该函数。
二、信号
在上面的例程中,我们可以看到在使用kill杀死程序时,while后面的两行代码并不会被执行,这是因为kill默认会让程序直接终止,而不是跳出while循环,如果我们想要程序完美退出,则需要啊了解一下Linux下的信号机制。
软中断信号(信号)用来通知进程发生了异步事件。在软件层次上是对中段机制的一种模拟;在原理上,一个进程收到一个信号处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中瘟疫的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号戴迪什么时候到达。进程之间可以互相通过系统调用kill()发送软中加信息。收到信号的进程有不同的处理方法。处理方法可以分为3类:
1.第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定函数,由该函数处理;
2.第二种方法是,忽略某个信号,对该信号不做任何处理,就像未发生一样;
3.第三种方法是,对该信号的处理保留系统默认值,这种缺省操作,对大部分的信号的缺省操作是使程序终止。进程通过调用singal来指定进程对某个信号的处理行为。
我们可以使用 kill -l命令查看当前系统支持的信号,需要注意的是不同的系统支持的信号是不一样的:
信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。对于目前Linux的两个信号安装函数:signal()及sigaction()来说,他们不能把SIGRTIMIN以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对SIGRTMIN以后的信号都支持排队。这两个函数最大的区别是经过sigaction安装的信号都能传递信息给信号处理函数,而经过signal安装的信号不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。
在上面的信号,我们常见或用到的是:
信号名 | 信号说明 | 默认动作 |
SIGINT | Ctrl+C按键终止程序运行的信号 | 程序终止 |
SIGILL | 非法的指令 | 程序终止 |
SIGBUS | 运行非本CPU相关编译器编译的程序 | 程序终止 |
SIGKILL | 强制杀死程序信号,任何程序都不可以捕捉该信号 | 程序终止,不可被捕捉 |
SIGUSR1 | 用户自定义信号1 | 程序终止 |
SIGSEGV | 段错误系统给程序发送的信号 | 程序终止 |
SIGUSR2 | 用户自定义信号2 | 程序终止 |
SIGPIPE | 管道破裂信号 | 程序终止 |
SIGALRM | alarm()系统调用发送的信号 | 程序终止 |
SIGTERM | kill命令默认发送信号,默认动作是终止信号 | 程序终止 |
signal()函数的原型如下:
#include<signal.h>
typedef void (*sighandleer_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
下面通过一个C程序示例来讲解这两个函数安装信号的使用
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
#include<execinfo.h>
int g_sigstop = 0;
void signal_stop(int signum)
{
if(SIGTERM == signum)
{
printf("SIGTERM signal detected\n");
}
else if(SIGALRM == signum)
{
printf("SIGALRM signal detected\n");
g_sigstop = 1;
}
}
void signal_user(int signum)
{
if(SIGUSR1 == signum)
{
printf("SIGUSR1 signal detected\n");
}
g_sigstop = 1;
}
void signal_code(int signum)
{
if(SIGBUS == signum)
{
printf("SIGBUS signal detected\n");
}
else if(SIGILL == signum)
{
printf("SIGILL signal detected\n");
}
else if(SIGSEGV == signum)
{
printf("SIGSEGV signal detected\n");
}
exit(-1);
}
int main(int argc,char **argv)
{
char *ptr = NULL;
struct sigaction sigact,sigign;
/*+--------------------------------------+
* | Method1:use signal() install signal |
* +-------------------------------------+*/
signal(SIGTERM,signal_stop);
signal(SIGALRM,signal_stop);
signal(SIGBUS,signal_code);
signal(SIGILL,signal_code);
signal(SIGSEGV,signal_code);
/* +-----------------------------------------+
* | Method2:use signaction() install signal |
* +----------------------------------------+*/
/*Initialize the catch signal structure*/
sigemptyset(&sigact.sa_mask);
sigact.sa_flags = 0;
sigact.sa_handler = signal_user;
/*Setup the ignore signal*/
sigemptyset(&sigign.sa_mask);
sigign.sa_flags = 0;
sigign.sa_handler = SIG_IGN;
sigaction(SIGINT,&sigign,0);/*ignore SIGINT signal by CTRL+C */
sigaction(SIGUSR1,&sigact,0);/*catch SIGUSR1*/
sigaction(SIGUSR2,&sigact,0);/*catch SIGUSR2*/
printf("Program start running for 20 seconds...\n");
alarm(20);
while(!g_sigstop)
{
;
}
printf("Program start stop running...\n");
printf("Inbalid pointer operator will raise SIGSEGV signal\n");
*ptr = 'h';
return 0;
}
运行结果如下:
在运行这个程序时我们发现Ctrl+C不能使程序终止运行,这是因为在上述代码中设置忽略了SIG_INT,所以程序在运行中忽略了Ctrl+C
在我们输入killall signal后程序发生段错误 运行如下:
三、多线程编程
在操作系统原理中,线程是进程的一条执行路径。线程在Unix系统下,通常被称为轻量级的进程。 所有的线程都是在同一进程空间运行,这也意味着多条线程将共享该进程中的全部系统资源;但同一进程中的多个线程各自有各自的调用栈,自己的寄存器环境,自己的线程本地存储。一个进程可以有多条线程,每条线程执行不同的任务。
1.创建线程
一个进程创建后,会首先生成一个缺省的线程,通常叫这个线程为主线程。创建线程使用pthread_create()函数,该函数原型如下:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数说明:thread:一个指向
pthread_t
类型变量的指针,在新线程创建后,线程 ID 将存储在这里。该 ID 用于在后续的 pthreads 调用中引用该线程。attr:一个可选的指向
pthread_attr_t
结构的指针,用于指定新线程的各种属性。如果传递了NULL
,则使用默认线程属性。start_routine:是一个函数指针,它指向的函数原型是 void *func(void *),这是所创建的子线程要执行的任务(函数)。
arg:传递给
start_routine
函数的参数。
pthread_attr_t类型,定义如下:
typedef stuuct
{
int detachstate; 线程的分离状态
int schedpolicy; 线程的调度策略
struct sched_parm schedparam; 线程的调度参数
int inheritsched; 线程的继承性
int scope; 线程的作用域
size_t guardsize; 线程栈末尾的警戒缓冲区大小
int stackaddr_set;
void * stackaddr; 线程栈的设置
size_t stacksize; 线程栈的大小
}pthread_attr_t;
对于这些属性,我们需要设定线程的分离状态,如果有需要也要修改每个栈的大小。每个线程默认是joinable状态,该状态需要主线程调用pthread_jion等待他退出,否则在子线程结束时,内存资源不能得到释放造成内存泄漏。所以我们创建线程时一般将线程设置为分离状态,有以下两种方法:
(1)线程里面调用pthread_detach(pthread_self())这个方法最简单;
(2)在创建线程的属性设置里设置PTHREAD_CREATE_DETACHED属性
子线程和主线程的默认关系是:无论子线程执行完毕与否,一旦主线程执行完毕退出,所有子线程都会终止运行。这是整个进程结束或僵死,部分线程保持一种中止执行但是还未销毁的状态,而进程必须在其所有线程销毁后才能销毁,现在进程处于僵死状态。线程函数执行完毕退出,或以其他非常方式终止,线程进入终止态,但是线程之前所占用的资源不一定被释放,可能在系统重启前都未能释放。终止态的线程仍然作为一个线程实体存在操作系统中,什么时候销毁,取决于线程属性。在这种情况下,主线程和子线程通常有两种关系:
(1)可会合:在这种关系下,主线程需要明确执行等待操作,在子线程执行完毕后,主线程的等待操作执行完毕,子线程和主线程汇合,这时主线程执行的等待操作后的下一步操作。在主线程的线程函数内部调用子线程对象的内部函数wait函数实现,即使子线程能够在等待操作之前完成操作进入终止态,也必须要执行汇合操作,否则,系统永远不会主动销毁线程,非赔给线程的系统资源等也不会被释放;
(2)相分离:表示子线程无需跟主线程会合,也就是相分离的。在这种情况下,子线程一旦进入到终止状态,有时让主线程等待子线程结束或者让主线程安排每个子线程结束的等待顺序,是很困难或不可能的,所以在并发子线程比较多的情况下,这种方式也会经常使用。
线程的分离状态决定一个线程以什么样的方式来终止自己,在默认情况下线程是非分离状态的,在这种情况下,原有的线程等待创建的线程结束,只有当pthread_join函数返回时,创建的线程才算终止,释放自己占用的系统资源,而分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。
pthread_join()函数是用于等待指定线程终止。pthread_join
会阻塞调用线程,直到指定的线程(由 thread
参数指定)终止为止。一旦线程终止,调用线程将继续执行,并且可以通过 retval
参数获取线程的退出状态。其原型如下:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
参数说明:thread:要等待的线程的线程 ID。
retval:用于存储线程的退出状态的指针。如果不关心线程的退出状态,可以将此参数设置为
NULL
。
一个简单的C程序来演示线程创建和基本的使用和相关概念:
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
void *thread_worker1(void *args);
void *thread_worker2(void *args);
int main(int argc,char **argv)
{
int shared_var = 1000;
pthread_t tid;
pthread_attr_t thread_attr;
/*pthread_addr_init()用默认值对现成属性对象进行初始化,&thread_attr指属性对象的地址*/
if(pthread_attr_init(&thread_attr))
{
printf("pthread_addr_init() failure : %s\n",strerror(errno));
return -1;
}
/*设置子线程中栈的大小*/
if( pthread_attr_setstacksize(&thread_attr,120*1024))
{
printf("pthread_attr_setstacksize() failure : %s\n",strerror(errno));
return -1;
}
/*设置线程是非分离状态还是分离状态,下面设置的是分离状态*/
if(pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED))
{
printf("pthread_addr_setdetachstate() failure : %s\n",strerror(errno));
return -1;
}
/*创建子线程worker1和worker2*/
pthread_create(&tid,&thread_attr,thread_worker1,&shared_var);
printf("Thread worker1 tid[%ld] created ok\n",tid);
pthread_create(&tid,NULL,thread_worker2,&shared_var);
printf("Thread worker2 tid[%ld] created ok\n",tid);
/*pthread_attr_destory() 函数的意思是销毁线程的属性结构体,使它未初始化不能再次使用*/
pthread_attr_destroy(&thread_attr);
/*Wait until thread worker2 exit()[程序会阻塞在这里不会继续向下执行]*/
pthread_join(tid,NULL);
while(1)
{
printf("Main/Control thread shared_var : %d\n",shared_var);
sleep(10);
}
}
void *thread_worker1(void *args)
{
int *ptr = (int *)args;
if( !args )
{
printf("%s() get invalid arguments\n",__FUNCTION__);
pthread_exit(NULL);
}
printf("Thread worker1[%ld] start running...\n",pthread_self());
while(1)
{
printf("+++ : %s before shared_var++ : %d\n",__FUNCTION__, *ptr);
*ptr += 1;
sleep(2);
printf("+++ : %s after sleep shared_var : %d\n",__FUNCTION__, *ptr);
}
printf("Thread worker1 exit...\n");
return NULL;
}
void *thread_worker2(void *args)
{
int *ptr = (int *)args;
if( !args )
{
printf("%s() get invalid arguments\n",__FUNCTION__);
pthread_exit(NULL);
}
printf("Thread worker2[%ld] start running...\n",pthread_self());
while(1)
{
printf("--- : %s before shared_var++ : %d\n",__FUNCTION__, *ptr);
*ptr += 1;
sleep(2);
printf("--- : %s after sleep shared_var : %d\n",__FUNCTION__, *ptr);
}
printf("Thread worker2 exit...\n");
return NULL;
}
程序运行结果:
主线程创建子线程之后究竟是子线程先运行还是主线程先运行系统并没有明确规定,这主要依赖于系统的调度策略 。因为在上述代码调用了pthread_join()函数导致主线程阻塞,所以主线程不会往下继续执行while(1)循环。
我们在创建子线程之后,在子线程的执行函数里面通常存在一个while的死循环来让子线程一直运行,否则子线程将按照代码顺序执行,执行完毕县城就自动退出。同样的住线程也应该添加一个while循环,否则主线程退出,进程就退出,那它的子线程液就全部退出了。
从上面的运行结果我们可以看到,在线程创建好之后worker1线程开始运行,它自加了第一次,使shared_var自加输出1001之后休眠2s;但是在他第二次输出时shared_var的值编程1002。这是由于shared_var这个变量会同时被子线程worker1和worker2共享访问修改导致。如果一个资源会被不同的线程访问修改,那么我们把这个资源叫做临界资源,对于该资源访问修改相关代码就叫做临界区。例如上述的代码
while(1)
{
printf("+++ : %s before shared_var++ : %d\n",__FUNCTION__, *ptr);
*ptr += 1;
sleep(2);
printf("+++ : %s after sleep shared_var : %d\n",__FUNCTION__, *ptr);}
就叫做临界区。
对于怎么解决多个线程之间共享一个共享资源,是多线程需要解决的一个问题,所以下一篇文章开始讲述互斥锁,用互斥锁解决上述问题。