守护进程和线程

守护进程和线程

守护进程

Daemon(精灵)进程,是Linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件一般采用以d结尾的名字,如vsftpd

Linux后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现;ftp服务器;nfs服务器等

进程组和会话

ps ajx查看进程组ID和会话ID

进程组

进程组是一个或者多个进程的集合,每个进程都属于一个进程组,引入进程组是为了简化对进程的管理。当父进程创建子进程的时候,默认子进程与父进程属于同一个进程组

进程组ID第一个进程ID(组长进程)。如父进程创建了多个子进程,父进程和多个子进程同属于一个组,而由于父进程是进程组里的第一个进程,所以父进程就是这个组的组长, 组长ID父进程ID

可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀死

只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关

进程组生存期:从进程组创建到最后一个进程离开

会话

  • 一个会话是一个或多个进程组的集合
  • 创建会话的进程不能是进程组组长
  • 创建会话的进程成为一个进程组的组长进程,同时也成为会话的会长
  • 需要有root权限(ubuntu不需要)
  • 新创建的会话丢弃原有的控制终端
  • 建立新会话时,先调用fork, 父进程终止,子进程调用setsid函数

image-20211124222937063

创建守护进程模型

  1. fork子进程,父进程退出

子进程继承了父进程的进程组ID, 但具有一个新的进程ID,这样就保证了子进程不是一个进程组的组长ID,这对于下面要做的setsid函数的调用是必要的前提条件

  1. 子进程调用setsid函数创建新会话

调用这个函数以后

  • 该进程成为新会话的首进程,是会话的会长
  • 成为一个新进程组的组长进程,是进程组组长
  • 不受控制终端的影响
  1. 改变当前工作目录chdir

如:a.out在U盘上,启动这个程序,这个程序的当前的工作目录就是这个u盘,如果u盘拔掉后进程的当前工作目录将消失,a.out将不能正常工作。

  1. 重设文件掩码 mode & ~umask
  • 子进程会继承父进程的掩码
  • 增加子进程程序操作的灵活性
  • umask(0000);
  1. 关闭文件描述符
  • 守护进程不受控制终端的影响所以可以关闭,以释放资
  • close(STDIN_FILENO);
  • close(STDOUT_FILENO);
  • close(STDERR_FILENO);
  1. 执行核心工作:守护进程的核心代码逻辑

实例

编写一个守护进程,每隔2S钟获取一次系统时间,并将这个时间写入磁盘文件

//创建守护进程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/time.h>
#include<signal.h>
#include<time.h>
#include<fcntl.h>
#include<sys/stat.h>

void myfunc(int signo)
{
    
    
    //打开文件
    int fd=open("mydemon.log",O_RDWR|O_CREAT,0755);
    if(fd<0)
    {
    
    
        return;
    }
    //获取当前系统时间
    time_t t;
    time(&t);
    char *p=ctime(&t);

    //将时间写入文件
    write(fd,p,strlen(p));
    close(fd);
    return;
}

int main()
{
    
    
    //父进程fork子进程,父进程退出
    pid_t pid=fork();
    if(pid<0||pid>0)
    {
    
    
        exit(1);
    }

    //子进程调用setsid函数创建会话
    setsid();

    //改变当前工作目录
    chdir("/usr/local/src/test01");

    //改变文件掩码
    umask(0000);

    //关闭标准输入、输出、错误输出文件描述符
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    //核心操作
    
    //注册信号处理函数
    struct sigaction act;
    act.sa_handler=myfunc;
    act.sa_flags=0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM,&act,NULL);

    //设置时钟
    struct itimerval tm;
    tm.it_interval.tv_sec=2;
    tm.it_interval.tv_usec=0;
    tm.it_value.tv_sec=3;
    tm.it_value.tv_usec=0;
    setitimer(ITIMER_REAL,&tm,NULL);

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

线程

什么是线程

轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程

进程:拥有独立的地址空间,拥有PCB,相当于独居

线程:有PCB,但没有独立的地址空间,多个线程共享进程空间,相当于合租

image-20211125133010494

在Linux操作系统下,线程是最小的执行单位,进程是最小分配资源单位

ps -Lf pid查看指定线程的LWP号

image-20211125133201619

无论是创建进程fork,还是创建线程pthread_create,底层实现都是调用同一个内核函数 clone

线程所有操作函数 pthread_* 是库函数,而非系统调用

Linux内核不区分进程和线程,只在用户层面上区分

线程共享资源

  • 文件描述符表
  • 每种信号的处理方式
  • 当前工作目录
  • 用户ID和组ID
  • 内存地址空间 (.text/.data/.bss/heap/共享库)

线程非共享资源

  • 线程ID
  • 处理器线程和栈指针(内核栈)
  • 独立的栈空间(用户空间栈)
  • errno变量
  • 信号屏蔽字
  • 调度优先级

优点

  • 提高程序并发性
  • 开销小
  • 数据通信、共享数据方便

缺点

  • 库函数,不稳定
  • gdb调试、编写困难
  • 对幸好支持不好

pthread_create

//创建一个新线程

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

//成功,返回0 失败,返回错误号

//pthread_t:传出参数,保存系统为我们分配好的线程ID
//attr:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。
//start_routine:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。
//arg:线程主函数执行期间所使用的参数。

//由于pthread_create的错误码不保存在errno中,因此不能直接用perror()打印错误信息,可以先用strerror()把错误码转换成错误信息再打印

//如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行
//创建子线程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>

void* mythread(void *arg)
{
    
    
    printf("child thread,pid==[%d],id==[%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("main thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
    sleep(1);
    return 0;
}

image-20211125135610004

循环创建多个子线程

//循环创建子线程,并且打印是第几个子线程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>

void* mythread(void *arg)
{
    
    
    int i=*(int*)arg;
    printf("[%d]:child thread,pid==[%d],id==[%ld]\n",i,getpid(),pthread_self());
}

int main()
{
    
    
    int n=5;
    int i=0;
    //创建子线程
    pthread_t thread[5];
    for(i=0;i<5;i++)
    {
    
    
        int ret=pthread_create(&thread[i],NULL,mythread,&i);
        if(ret!=0)
        {
    
    
            printf("pthread_create error,[%s]\n",strerror(ret));
            return -1;
        }
    }
    printf("main thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
    sleep(1);
    return 0;
}

image-20211125140218745

i全部为5

在创建子线程的时候使用循环因子作为参数传递给子线程,这样主线程和多个子线程就会共享变量i(变量i在main函数中定义,在整个进程都一直有效)所以在子线程看来变量i是合法的栈内存空间

那么为什么最后每个子线程打印出来的值都是5呢

是由于主线程可能会在一个cpu时间片内连续创建了5个子线程,此时变量i的值变成了5,当主线程失去cpu的时间片后,子线程得到cpu的时间片,子线程访问的是变量i的内存空间的值,所以打印出来值为5

image-20211125140408189

解决方法

不能使多个子线程都共享同一块内存空间,应该使每个子线程访问不同的内存空间,可以在主线程定义一个数组:int arr[5];,然后创建线程的时候分别传递不同的数组元素,这样每个子线程访问的就是互不相同的内存空间,这样就可以打印正确的值

image-20211125140505329

如果主线程早于子线程退出,则子线程可能得不到执行,因为主线程退出,整个进程空间都会被回收,子线程没有了生存空间,所以也就得不到执行

线程之间(包含主线程和子线程)可以共享同一变量,包含全局变量或者非全局变量(但是非全局变量必须在其有效的生存期内)

//循环创建子线程,并且打印是第几个子线程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>

void* mythread(void *arg)
{
    
    
    int i=*(int*)arg;
    printf("[%d]:child thread,pid==[%d],id==[%ld]\n",i,getpid(),pthread_self());
}

int main()
{
    
    
    int n=5;
    int i=0;
    int a[5];
    //创建子线程
    pthread_t thread[5];
    for(i=0;i<5;i++)
    {
    
    
        a[i]=i+1;
        int ret=pthread_create(&thread[i],NULL,mythread,&a[i]);
        if(ret!=0)
        {
    
    
            printf("pthread_create error,[%s]\n",strerror(ret));
            return -1;
        }
    }
    printf("main thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
    sleep(1);
    return 0;
}

image-20211125140638259

pthread_exit

在线程中禁止调用exit函数,否则会导致整个进程退出,取而代之的是调用pthread_exit函数,这个函数是使一个线程退出,如果主线程调用pthread_exit函数也不会使整个进程退出,不影响其他线程的执行

//将单个线程退出

void pthread_exit(void *retval);	

//retval表示线程退出状态,通常传NULL

//pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了,栈空间就会被回收

pthread_join

//阻塞等待线程退出,获取线程退出状态。其作用,对应进程中的waitpid() 函数

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

//成功:0;失败:错误号

//thread:线程ID
//retval:存储线程结束状态,整个指针和pthread_exit的参数是同一块内存地址
//循环创建子线程,并且打印是第几个子线程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>

int g_var=9;

void* mythread(void *arg)
{
    
    
    printf("child thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
    printf("[%p]\n",&g_var);
    pthread_exit(&g_var);
}

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("main thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
   
    //回收子线程
    void* p=NULL;
    pthread_join(thread,&p);
    int a=*(int*)p;
    printf("child exit status:[%d],[%p]\n",a,p);
    return 0;
}

image-20211125142919651

pthread_detach

线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用

进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在

可使用 pthread_create函数参2(线程属性)来设置线程分离

pthread_detach函数是在创建线程之后调用的

//实现线程分离

int pthread_detach(pthread_t thread);	

//成功:0;失败:错误号

//一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了
//循环创建子线程,并且打印是第几个子线程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>

void* mythread(void *arg)
{
    
    
   while(1)
   {
    
    
       int a;
       int b;

       //设置取消点
       pthread_testcancel();
   }
}

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("main thread,pid==[%d],id==[%ld]\n",getpid(),pthread_self());
   
    //取消子线程
    pthread_cancel(thread);

    pthread_join(thread,NULL);
    return 0;
}

pthread_cancel

//杀死(取消)线程。其作用,对应进程中 kill() 函数

int pthread_cancel(pthread_t thread);	

//成功:0;失败:错误号

//线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)
//取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write..... 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。可粗略认为一个系统调用(进入内核)即为一个取消点。还以通过调用pthread_testcancel函数设置一个取消点

void pthread_testcancel(void);

pthread_equal

//比较两个线程ID是否相等

int pthread_equal(pthread_t t1, pthread_t t2);

线程属性

线程的分离状态决定一个线程以什么样的方式来终止自己,有两种状态:

  • 非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源
  • 分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态
//定义线程属性类型类型的变量
pthread_attr_t  attr;	

//对线程属性变量进行初始化
int pthread_attr_init (pthread_attr_t* attr);

//设置线程为分离属性
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
//attr: 线程属性
//detachstate:
//PTHREAD_CREATE_DETACHED(分离)
//PTHREAD_CREATE_JOINABLE(非分离)
//这一步完成之后调用pthread_create函数创建线程,则创建出来的线程就是分离线程;其实上述三步就是pthread_create的第二个参数做准备工作

//释放线程属性资源
int pthread_attr_destroy(pthread_attr_t *attr);
//参数:线程属性

线程同步

线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能

实例

创建两个线程,让两个线程共享一个全局变量int number, 然后让每个线程数5000次数,看最后打印出这个number值是多少

经过多次测试最后的结果显示,有可能会出现number值少于5000*2=10000的情况

原因:假如子线程A执行完了cur++操作,还没有将cur的值赋值给number失去了cpu的执行权,子线程B得到了cpu执行权,而子线程B最后执行完了number=cur,而后失去了cpu的执行权;此时子线程A又重新得到cpu的执行权,并执行number=cur操作,这样会把线程B刚刚写回number的值被覆盖了,造成number值不符合预期的值

数据混乱原因

  • 资源共享
  • 调度随机(线程操作共享资源的先后顺序不确定)
  • 线程间缺乏必要的同步机制

如何解决

  • 原子操作:该操作要么不做,要么就完成
  • 使用互斥锁:模拟原子操作

互斥锁

Linux中提供一把互斥锁mutex(也称之为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁

资源还是共享的,线程间也还是竞争的,但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了

线程1访问共享资源的时候要先判断锁是否锁着,如果锁着就阻塞等待;若锁是解开的就将这把锁加锁,此时可以访问共享资源,访问完成后释放锁,这样其他线程就有机会获得锁

同一时刻,只能有一个线程持有该锁,只要该线程未完成操作就不释放锁

使用互斥锁之后,两个线程由并行操作变成了串行操作,效率降低了,但是数据不一致的问题得到解决了

主要函数

//pthread_mutex_t 类型
//本质是一个结构体,为简化理解,应用时可简单当成整数看待
pthread_mutex_t mutex; 
//变量mutex只有两种取值1、0


//pthread_mutex_init函数
//初始化一个互斥锁(互斥量) ---> 初值可看作1
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
//mutex:传出参数,调用时应传 &mutex	
//attr:互斥锁属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)
//restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改互斥量mutex的两种初始化方式:
//静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化
pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
//动态初始化:局部变量应采用动态初始化
pthread_mutex_init(&mutex, NULL);


//pthread_mutex_destroy函数
//销毁一个互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//mutex—互斥锁变量


//pthread_mutex_lock函数
//对互斥所加锁,可理解为将mutex--
int pthread_mutex_lock(pthread_mutex_t *mutex);
//mutex—互斥锁变量


//pthread_mutex_unlock函数
//对互斥所解锁,可理解为将mutex ++
int pthread_mutex_unlock(pthread_mutex_t *mutex);


//pthread_mutex_trylock函数
//尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//mutex—互斥锁变量

加锁和解锁

lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止
unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒

猜你喜欢

转载自blog.csdn.net/blll0/article/details/121537807