作者介绍
张伟伟,男,西安工程大学电子信息学院,2019级硕士研究生,张宏伟人工智能课题组。
微信公众号:可随时查阅,搜索—张二牛的笔记,内容会分类上传。
研究方向:机器视觉与人工智能。
电子邮件:2430290933@qq.com
电子邮件:zhangweiweicpp@163.com
- 课题组CSDN官方账号,欢迎一键三连: https://blog.csdn.net/m0_37758063/article/details/113527955?spm=1001.2014.3001.5501.
学习目标
- 说出守护进程的特点
- 独立实现多个线程的创建
- 独立实现线程的退出和资源回收
- 理解线程同步的思想
1 守护进程(也称Daemon 精灵进程–》是后台服务进程)
是Linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字,如vsftpd
1.1 守护进程的特点
1.一个linux后台服务进程
2.不依赖于控制终端
3.周期性执行某些任务
4. 不受用户登录和注销的影响
5.一般采用以d结尾的名字
1.2 进程组和会话
#查看进程个数
ps -ef | wc -l
- 进程组
进程组是一个或者多个进程的集合,每个进程都属于一个进程组,引入进程组是为了简化对进程的管理。当父进程创建子进程的时候,默认子进程与父进程属于同一个进程组。
第一个进程ID(组长进程)
只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。
进程组生存期:从进程组创建到最后一个进程离开
- 会话(组长不能调用setsid创建会话)
一个会话是一个或多个进程组的集合。
创建会话的进程不能是进程组组长
创建会话的进程成为一个进程组的组长进程,同时也成为会话的会长。
需要有root权限(ubuntu不需要)
新创建的会话丢弃原有的控制终端
建立新会话时,先调用fork, 父进程终止,子进程调用setsid函数
可以使用ps ajx来查看进程组ID和会话ID
可以fork出几个子进程,然后查看进程组ID和会话ID
- 进程与会话的关系:
1.3 创建守护进程的模型
//星号必有
**第1步:fork子进程,父进程退出**
子进程继承了父进程的进程组ID, 但具有一个新的进程ID,这样就保证了子进程不是一个进程组的组长ID,这对于下面要做的setsid函数的调用是必要的前提条件
**第2步:子进程调用setsid()函数创建新会话**
调用这个函数以后
该进程成为新会话的首进程,是会话的会长
成为一个新进程组的组长进程,是进程组组长
不受控制终端的影响
第3步:改变当前工作目录chdir() 防止在可插拔的,非必须
如:a.out在U盘上,启动这个程序,这个程序的当前的工作目录就是这个u盘,如果u盘拔掉后进程的当前工作目录将消失,a.out将不能正常工作。
第4步:重设文件掩码(他自己,也是非必须的,一般为0002) mode & ~umask
子进程会继承父进程的掩码
增加子进程程序操作的灵活性
umask(0000);
第5步:关闭文件描述符
守护进程不受控制终端的影响所以可以关闭,以释放资源
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
**第6步:执行核心工作**
守护进程的核心代码逻辑
1.4 实例:创建守护进程模型的案例分析
编写一个守护进程,每隔2s种获取一次系统时间,并将这个时间写入磁盘文件
分析: 首先按照1.3介绍的守护进程的步骤创建一个守护进程
每隔2S种: 使用settimer函数设置时钟,该时钟发送的是SIGALRM信号,
信号操作:注册信号的处理函数,signal或者signalatcion注册信号处理函数
调用time函数,ctime函数。
写入磁盘文件;文件操作函数: open write close
1.4.1 创建守护进程模型代码实现
//创建守护进程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
#include<time.h>
#include<sys/time.h>
#include<sys/stat.h>
#include<fcntl.h>
void myfunc(int signo)
{
//打开文件
int fd = open("/data/Users/zhangww/log/mydemon.log",O_RDWR | O_CREAT | O_APPEND, 0755);
if(fd<0)
{
return;
}
time_t t;
time(&t);
char *p = ctime(&t);
//将时间写入文件
write(fd, p, strlen(p));
close(fd);
}
int main()
{
//父进程fork子进程,然后父进程退出
pid_t pid = fork();
if(pid<0 || pid >0)
{
exit(1);
}
//子进程调用fork函数创建会话
setsid();
//chdir 改变当前的工作目录/data/Users/zhangww/log
chdir("/data/Users/zhangww/log");
//改变文件掩码
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);
}
return 0;
}
2. 线程(pthread_create)
2.1 什么是线程 ?
轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。
2.2线程进程的区别
进程:拥有独立的地址空间,拥有PCB,相当于独居。
线程
有PCB,但没有独立的地址空间,多个线程共享进程空间,相当于合租。
1.线程:最小的执行单位
2. 进程:最小分配资源单位,可看成是只有一个线程的进程。
3.多个子线程和主线程共享一个地址空间,共用一个PID,
4.通过不同的线程号来区分不同的线程。
5.除了栈空间以外,其余资源都可以共享(线程里自己创建的变量。)(如main全局变量内存的生存期有效的,合法的,使用指针读取)
6.主线程和子线程谁先执行?
不一定,谁先抢到CPU资源谁先执行。
线程是轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
#查看指定线程的LWP:
ps -Lf pid
2.3 线程的共享资源与非共享资源
Linux内核是不区分进程和线程的, 只在用户层面上进行区分。
所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。
- 线程共享资源
文件描述符表
每种信号的处理方式
当前工作目录
用户ID和组ID
内存地址空间 (.text/.data/.bss/heap/共享库)
- 线程非共享资源
线程id
处理器现场和栈指针(内核栈)
独立的栈空间(用户空间栈)
errno变量(尽量用strerror()打印错误)
信号屏蔽字(尽量在线程中不使用信号)
调度优先级
多个子线程读全局变量防止乱套,必须要加锁。
2.4 线程的优缺点
优点:
提高程序并发性
开销小
数据通信、共享数据方便(内存共享)
缺点:
库函数,不稳定
gdb调试、编写困难
对信号支持不好
一般数据通信用线程,业务处理用进程操作。
2.5 创建线程(pthread_cread函数)
pthread_create函数
函数作用:
创建一个新线程
函数原型
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr, void *(*start_routine) (void *),void *arg);
返回值
成功,返回0
失败,返回错误号
函数参数:
pthread_t:传出参数,保存系统为我们分配好的线程ID
当前Linux中可理解为:typedef unsigned long int pthread_t。
attr:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。
start_routine:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。
arg:线程主函数执行期间所使用的参数。
注意点
由于pthread_create的错误码不保存在errno中,因此不能直接用perror()打印错误信息,可以先用strerror()把错误码转换成错误信息再打印。
如果任意一个线程调用了exit或_exit(慎用),则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行,下一节我们会看到更好的办法。
pthread_create.c
//创建子线程
#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()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
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;
}
附加:
问题:需要传参时,如何编写
问题2 出现打印时,共享内存,打印导致都是5的情况。
2.5 线程退出 pthread_eixt()
使用ps命令查看线程号
ps -ef
ps -Lf PID
函数描述
将单个线程退出
函数原型
void pthread_exit(void *retval);
函数参数
retval表示线程退出状态,通常传NULL
另注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了,栈空间就会被回收。
2.6 如何避免僵尸线程的产生 pthread_join函数
函数描述:阻塞等待线程退出,获取线程退出状态。其作用,对应进程中的waitpid() 函数。
函数原型:int pthread_join(pthread_t thread, void **retval);
函数返回值:
成功:0;
失败:错误号
函数参数:
thread:线程ID
retval:存储线程结束状态,整个指针和pthread_exit的参数是同一块内存地址。
练习:编写程序,使主线程获取子线程的退出状态。
一般先定义void *ptr; 然后pthread_join(threadid, &ptr);
//线程退出函数测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
struct Test
{
int data;
char name[64];
};
int g_var = 9;
struct Test t;
//线程执行函数
void *mythread(void *arg)
{
printf("child thread, pid==[%d], id==[%ld]\n", getpid(), pthread_self());
//printf("[%p]\n", &g_var);
//pthread_exit(&g_var);
memset(&t, 0x00, sizeof(t));
t.data = 99;
strcpy(t.name, "xiaowen");
pthread_exit(&t);
}
int main()
{
//int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
//创建子线程
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 n = *(int *)p;
struct Test *pt = (struct Test *)p;
printf("child exit status:[%d],[%s],[%p]\n", pt->data, pt->name, p);
return 0;
}
2.7 线程设置分离属性自己回收自己,pthread_detach 函数
线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用
- 1.在线程创建成功后设置
视频13的最会总结部分。明天看
2.9 取消线程 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);
线程来回切换时间片造成的:
- 数据混乱的原因
资源共享(独享资源则不会)
调度随机(线程操作共享资源的先后顺序不确定)
线程间缺乏必要的同步机制。
以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。
如何解决问题
原子操作的概念
原子操作指的是该操作要么不做,要么就完成。
使用互斥锁解决同步问题
使用互斥锁其实是模拟原子操作,互斥锁示意图:
线程1访问共享资源的时候要先判断锁是否锁着,如果锁着就阻塞等待;
若锁是解开的就将这把锁加锁,此时可以访问共享资源,访问完成后释放锁,这样其他线程就有机会获得锁。
应该注意:图中同一时刻,只能有一个线程持有该锁,只要该线程未完成操作就不释放锁。
使用互斥锁之后,两个线程由并行操作变成了串行操作,效率降低了,但是数据不一致的问题得到解决了。
同时不能用两个线程访问共享资源
3.0 互斥锁的使用步骤
1. 创建一把锁:pthread_mutex_t mutex;
2. 在main函数中初始化互斥锁:pthread_mutex_init(&mutex, NULL); //认为mutex为1,在线程中使用锁
3. 锁的使用----在共享资源出现的位置的上下加锁和解锁
pthread_mutex_lock(&mutex) mutex--
pthread_mutex_unlock(&mutex) mutex--
4.在main函数中释放互斥锁
pthread_mutex_destroy(&mutex);
3.1 互斥锁使用实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
//定义一把锁
pthread_mutex_t mutex;
void *mythread1(void *args)
{
while(1)
{
//加锁
pthread_mutex_lock(&mutex);
printf("hello ");
sleep(rand()%3);
printf("world\n");
//解锁
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
pthread_exit(NULL);
}
void *mythread2(void *args)
{
while(1)
{
//加锁
pthread_mutex_lock(&mutex);
printf("HELLO ");
sleep(rand()%3);
printf("WORLD\n");
//解锁
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
pthread_exit(NULL);
}
int main()
{
int ret;
pthread_t thread1;
pthread_t thread2;
//随机数种子
srand(time(NULL));
//互斥锁初始化
pthread_mutex_init(&mutex, NULL);
ret = pthread_create(&thread1, NULL, mythread1, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
ret = pthread_create(&thread2, NULL, mythread2, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
//释放互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
3.2 互斥锁的相关函数(了解使用即可,会查询)
pthread_mutex_t 类型
其本质是一个结构体,为简化理解,应用时可忽略其实现细节,简单当成整数看待。
1.pthread_mutex_t mutex; 变量mutex只有两种取值1、0。
pthread_mutex_init函数
函数描述:
初始化一个互斥锁(互斥量) ---> 初值可看作1
函数原型:
2. 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—互斥锁变量