Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)

一.守护进程

1.守护进程介绍:

        守护进程也叫精灵(deamon)进程,是Linux后台服务进程,通常独立于控制终端并且周期性的执行某些任务或者等待处理某些发生的事件。守护进程具有以下特点:它是后台服务进程,独立于控制终端,周期性的执行某种任务,不受用户登录和注销的影响。

2.进程组和会话

        进程组是一个或多个进程的集合,每个进程都属于一个进程组,引入进程组是为了简化进程的管理,当父进程创建子进程的时候,父子进程默认就在一个进程组内。进程组ID就等于第一个进程的ID也就是组长ID。比如说父进程创建了多个子进程,父进程和多个子进程就是属于一个进程组的,由于父进程是进程组的第一个进程,因此其就是进程组组长,父进程ID就是进程组ID,注意只有当一个进程组内的所有进程全都退出这个进程组才会消失。之前在说kill函数的时候,说到了其能够给进程组的所有进程发信号。

Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)_第1张图片

3.会话

        一个会话是一个或多个进程组的集合,注意创建会话有一个硬性条件就是:组长进程不能创建会话。创建会话的进程就会成为一个进程组的组长进程,同时也会称为会话组的会长,创建会话进程需要root权限,新创建的会话会丢弃原有的控制终端。我们可以通过ps -ajx来查看会话组和进程组ID:

Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)_第2张图片

4.创建守护进程

        前面了解了守护进程和会话组之后,现在就来看看如何创建守护进程,首先父进程调用fork函数创建子进程,然后父进程退出,然后子进程调用setsid函数创建会话,子进程就成为了会话组组长,不再受控制终端的影响,然后就是修改工作目录chdir,修改掩码umask(0000),关闭文件描述符(STDIN_FILENO STDOUT_FILENO STDERR_FILENO),最后是核心操作,就是你这个守护进程要在后台进程哪种操作,其中最重要的就是前面两步和最后一步,中间的都是可有可无的。因为那个修改工作目录就是把这个守护进程创建到别的目录下,修改掩码能够增加处理的灵活性,掩码表示的是权限,因为守护进程不受终端的影响,关闭文件描述符能够节约资源。根据上面的步骤,下面实操一下:编写一个守护进程每2s获取一次系统时间并将时间写入到磁盘文件.

        先简单的分析一下这个案例,首先每2s需要用到之前说过的setitimer函数设置时钟,该时钟发送SIGALRM信号;这里就涉及到了注册信号处理函数,signal和sigaction,以及一个信号处理函数;获取系统时间,还需要用到time函数,time函数的返回值是一个time_t类型,还需要使用

ctime函数将其转换成字符串,最后涉及到文件操作,open write close 等函数。Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)_第3张图片

#include
#include
#include
#include
#include
#include
#include
#include
#include

void handler(int signo)
{   
    int fd = open("mydeamon.log",O_RDWR | O_CREAT | O_APPEND);
    if(fd<0)
    {   
        return ;
    }
    time_t t; 
    time(&t); 
    char *p = ctime(&t);
    write(fd,p,sizeof(p));
    close(fd);
}
int main()
{
    //父进程创建子进程,父进程退出
    pid_t pid = fork();
    if(pid<0 || pid>0)
    {   
        exit(1);
    }   
    //子进程创建会话
    setsid();
    //修改当前目录
    //chdir();
    umask(0000);//设置掩码
    //关闭文件描述符
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    //核心操作
    struct itimerval st; 
    st.it_interval.tv_sec=2;
    st.it_interval.tv_usec=0;
    st.it_value.tv_sec=2;
    st.it_value.tv_usec=0;
    setitimer(ITIMER_REAL,&st,NULL);
 //注册信号处理函数sigaction
    struct sigaction act;
    act.sa_handler=handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags=0;
    sigaction(SIGALRM,&act,NULL);

    while(1)
    {
        sleep(1);
    }
    return 0;
}

       这里我就没有进行工作目录的修改,chdir在使用的时候参数就是你要把这个文件放到哪个目录下,就传入哪个目录,最重要的还是要记住这个创建守护进程的套路。 这里的核心操作就是设置时钟和注册信号处理函数并在信号处理函数里面完成获取时间将时间写入到文件中,理清逻辑后,就很简单了,按照步骤来,就只是核心操作部分需要自己编写,其余部分几乎都是一样的套路。

        当编译完成后启动这个程序时候发现没反应,这是因为守护进程是运行在后台的,这时候我们可以先通过观察日志的方式观察写入的文件的内容的变化,或者是观察后台的进程的运行。

         这个时候杀死守护进程只能通过kill命令了。

二.线程

1.线程概念:

        线程在Linux下是轻量级进程,线程有PCB,但是没有独立的地址空间,多个线程共享进程空间。还有我们熟悉的线程是调度的基本单位。多个子线程公用一个进程空间,只有一个PID,但是我们可以通过线程号来区别不同的线程。除了栈空间以外,其他的资源都是可以共享的。

Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)_第4张图片

2.线程的创建

        线程的创建使用的是pthread_create()函数,函数原型如下。使用的时候需要用到头文件,还有编译的时候需要在后面加上 -pthread。

 int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

        第一个参数是一个传出参数,表示的是线程ID,第二个参数是一个传入参数,可以用来设置线程的分离属性,第三个参数是线程的回调函数,第四个参数是线程回调函数的参数。对于返回值,当返回0的时候表示创建成功,创建失败则返回相应的错误号,不过这里有一点需要注意的是打印错误的时候需要用strerror()函数,因为pthread_create()的错误码不保存在errno中,因此要先把错误码转换成错误信息在打印。下面先看一个简单的创建线程的例子:

#include
#include
#include
#include
#include
#include

void *mythread(void *args)
{
    printf("process id[%d] threadID[%ld]\n",getpid(),pthread_self());
}
int main()
{
    pthread_t thread;
    int ret = pthread_create(&thread,NULL,mythread,NULL);
    if(ret!=0)
    {
        printf("pthread_create error [%s]\n",strerror(ret));
        return -1;
    }
    printf("process id[%d] threadID[%ld]\n",getpid(),thread);
    sleep(1);//为了让子线程执行起来
    return 0;
}

        这里我们用到了pthread_self打印线程ID;注意线程的回调函数是void * 的,参数也是;在主线程中我们通过创建返回的thread来打印线程ID,在回调函数中使用pthread_self();最后发现这俩的值是一样的,同时也证明了进程和线程公用一个地址空间。

3.线程创建时的参数传递问题

         因为参数类型是void*类型的,因此可以传任意类型的参数,下面就简单举几个例子:直接在上面的代码上进行简单的修改即可:

Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)_第5张图片

        先来一个传入普通变量的,最后在回调函数里面需要给转回相应的类型。

        再来一个传入自定义类型的变量:

Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)_第6张图片

        然后输出结果:

4.循环创建子线程

        我们的思路是直接利用一个循环进行创建,同时我们将循环变量i的值传入最后打印出是第几个线程:

Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)_第7张图片

        表面上看上去这个思路没有任何问题,但是看一下运行结果:打印出来的五个序号全都是5,并不是按照顺序来打印的,分析一下这里的i只能被主线程改变,某一时刻,主线程获得时间片,一下将五个子线程全部创建完成,然后主线程让出时间片,轮到子线程执行,此时传过去的i值都是5,这就是为什么全部都是5的原因,那么要如何修改才能避免这个问题呢?在这里这五个子线程是共享这个i的内存的,因此i只会有一个值,我们可以把i的内存看成五个部分,每个线程独占一份----------我们就可以通过数组来解决这个问题。

Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)_第8张图片         完整的代码如下:

#include
#include
#include
#include
#include
#include

void *mythread(void * args)
{
    int n = *(int *)args;
    printf("thread %d threadid = %ld\n",n,pthread_self());
}

int main()
{
    int i=0;
    int n=5;
    pthread_t thread[5];
    int arr[5];
    for(i=0;i<5;i++)
    {   
        arr[i]=i+1;
        int ret = pthread_create(&thread[i],NULL,mythread,&arr[i]);
        if(ret!=0)
        {   
            printf("pthread_create error %s\n",strerror(ret));
            return -1; 
        }   
        printf("PID = [%d] thread = [%d]\n",getpid(),thread[i]);
    }   

    sleep(1);

    return 0;
}

        不过需要注意的是线程的执行顺序是根据谁先抢到cpu谁就先执行的,所以运行结果可能不是按照顺序输出的。

Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)_第9张图片 5.线程退出函数和线程回收函数

        线程退出函数pthread_exit(),要注意的是这个函数的参数必须是创建在堆区上的,也就是必须是malloc出来的或者是一个全局变量,不使用这个参数的时候就可以直接传NULL,这个函数会让一个进程退出。

 void pthread_exit(void *retval);
#include
#include
#include
#include
#include
#include


void *mythread(void *args)
{
    printf("process id[%d] threadID[%ld]\n",getpid(),pthread_self());
    sleep(100);
}

int main()
{
    //int n=666;
    pthread_t thread;
    //int ret = pthread_create(&thread,NULL,mythread,&n);
    int ret = pthread_create(&thread,NULL,mythread,NULL);
    if(ret!=0)
    {   
        printf("pthread_create error [%s]\n",strerror(ret));
        return -1; 
    }   
    printf("process id[%d] threadID[%ld]\n",getpid(),thread);
    pthread_exit(NULL);
    sleep(1);//为了让子线程执行起来
    return 0;
}

        这里我们简单举个例子:使用pthread_exit函数使主线程退出,观察其主线程推出后是否会影响到子线程的执行。这里我在子线程设置了一个sleep就是为了观察主线程退出后其状态如何。

         通过运行结果我们发现主线程退出后子线程依旧可以执行的,但是观察一下后台进程的运行情况:这个进程变成了僵尸进程。

        这就需要pthread_join---线程阻塞函数出场了,其作用就类似于waitpid,回收子线程。注意这个函数在主线程中调用。

int pthread_join(pthread_t thread, void **retval);

        值得注意的是这个retval指向pthread_exit中的的retval。下面举个例子来验证一下。

#include
#include
#include
#include
#include
#include

int n=5;
void *mythread(void *args)
{
    
    printf("process id[%d] threadID[%ld]\n",getpid(),pthread_self());
    printf("n==%d  address %p\n",n,&n);
    pthread_exit(&n);

}

int main()
{
    //int n=666;
    pthread_t thread;
    //int ret = pthread_create(&thread,NULL,mythread,&n);
    int ret = pthread_create(&thread,NULL,mythread,NULL);
    if(ret!=0)
    {   
        printf("pthread_create error [%s]\n",strerror(ret));
        return -1; 
    }   
    printf("process id[%d] threadID[%ld]\n",getpid(),thread);
    void *p=NULL;
    pthread_join(thread,&p);
    int m=*(int *)p;
    printf("p==%d address %p\n",m,p);
    return 0;
}

        这里我们传入的就是全局变量,对于pthread_join的参数里面的二级指针可以定义一个void *的一级指针然后传入其地址,最后输出之前先强制转换一下就行,打印p和n的地址,发现是一样的。

Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)_第10张图片

 6.设置线程分离属性

        对于线程的分离属性,有两种方式可以实现,首先就是pthread_detach函数,首先明确点一点,线程默认的属性是非分离的,调用这个函数之后,线程结束退出后,其退出状态不由其他的线程获取,而直接自己自动释放。

int pthread_detach(pthread_t thread);

        当线程设置了分离属性后,再调用pthread_join函数将不会阻塞,而是会直接返回。因此可以使用这个来检测分离属性是否设置成功。对于pthread_detach,直接传入线程ID即可。

#include
#include
#include
#include
#include
#include

void *mythread(void *args)
{
    printf("process id[%d] threadID[%ld]\n",getpid(),pthread_self());
}

int main()
{
    pthread_t thread;
    int ret = pthread_create(&thread,NULL,mythread,NULL);
    if(ret!=0)
    {   
        printf("pthread_create error [%s]\n",strerror(ret));
        return -1; 
    }   
    printf("process id[%d] threadID[%ld]\n",getpid(),thread);
    pthread_detach(thread);

    ret=pthread_join(thread,NULL);  
    if(ret!=0)
    {   
        printf("pthread_join error %s\n",strerror(ret));
    }   
    sleep(1);
    return 0;
}

        设置分离属性后,pthread_join函数调用会失败。前面在讲到pthread_create的时候提到过它的第二个参数可以设置线程分离,下面就来了解一下如何设置吧:

         使用pthread_detach函数设置线程分离是在线程创建之后设置,这里是在线程创建的时候设置的线程分离,其有以下几个步骤:

//定义一个pthread_attr_t 变量
pthread_attr_t attr;
//调用pthread_attr_init 初始化这个变量
int pthread_attr_init(pthread_attr_t *attr);
//调用pthread_attr_setdetachstate 设置分离 第二个参数PTHREAD_CREATE_DETACHED
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
调用pthread_create 创建子线程并设置子线程分离
调用pthread_join函数验证是否分离
//最后调用pthread_attr_destroy()销毁attr变量
int pthread_attr_destroy(pthread_attr_t *attr);

        结合这个步骤下面看一个具体的例子:

#include
#include
#include
#include
#include
#include

void *mythread(void *args)
{
        printf("process id[%d] threadID[%ld]\n",getpid(),pthread_self());
}
int main()
{
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
    pthread_t thread;
    int ret = pthread_create(&thread,&attr,mythread,NULL);
    if(ret!=0)
    {   
        printf("pthread_create error %s\n",strerror(ret));
        return -1;  
    }   
    printf("main thread id [%d] child thread id [%d]\n",getpid(),thread);
    ret = pthread_join(thread,NULL);
    if(ret!=0)
    {   
        printf("pthread_join error %s\n",strerror(ret));
    }   
    sleep(1);
    pthread_attr_destroy(&attr);
    return 0;
}

        这里在pthread_create中设置线程分离只要按照指定的步骤来就可以很简单的实现,就是最后注意要回收一下。

7.线程取消

        说到线程的取消就是要调用pthread_cancel函数,其有一个参数是线程id,让指定的线程取消;那么在调用线程取消函数时首先明确一个概念就是--取消点,取消点就是检查线程是否被取消,并按请求进行动作的一个位置。通常是一些系统调用create,open,read,write.....可以粗略的认为系统调用即为一个取消点,当一个线程没有取消点是不能被取消的,当然我们也有能够设置取消点的函数--pthread_testcancel,这个参数没有参数,就是用来设置取消点的;下面看一个例子。首先在子线程中不写带有取消点的函数,发现其会阻塞;然后再在子线程中设置一个取消点;在观察;

Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)_第11张图片         这里补充一个可以观察子线程状态的命令:

ps -Lf +进程ID

         使用这个命令首先要用ps -ef 命令查看当前进程的id,然后通过这个命令查看:

Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)_第12张图片

        可以观察到两个线程都活着。下面在子线程加入取消点:

Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)_第13张图片

         会发现这个程序会立即返回而不是阻塞在那个循环中。

#include
#include
#include
#include
#include
#include

void* mythread()
{
    while(1)
    {   
        int a;
        int v;
        pthread_testcancel();
    }   
}
int main()
{
    pthread_t thread;
    int ret = pthread_create(&thread,NULL,mythread,NULL);
    if(ret!=0)
    {   
        printf("error reason %s\n",strerror(ret));
        return -1; 
    }   
    printf("child thread id=[%d],pid=[%d]\n",pthread_self(),getpid());
    pthread_cancel(thread);
    pthread_join(thread,NULL);
    return 0;
}

        注意一下这里使用printf或者是其他的系统调用函数都是可行的,都可以代替pthread_testacncel。

8.线程ID比较函数

        最后来简单了解一下一个线程ID比较函数---pthread_euqal,如果返回0说明不相等,返回其他的值都说明两个线程ID是相等的。

int pthread_equal(pthread_t t1, pthread_t t2);

Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)_第14张图片

Linux之守护进程和线程初步(守护进程的介绍和创建;线程的创建,设置线程分离....pthread)_第15张图片         这个了解即可,因为线程ID都是整形值,直接利用等号比较即可。

你可能感兴趣的:(Linux,linux,服务器,运维)