目录
代码: https://github.com/WHaoL/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
代码: https://gitee.com/liangwenhao/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
1. 守护进程
守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。
1.1 守护进程的特点
- 守护进程命名: 名字末尾结束字符d (一般情况下是这样的, 不是必须的)
- 不依赖终端 (一般没有终端)
- 用户不能和这个进程交互
- 不能输入
- 不能输出
- 用户不能和这个进程交互
- 注销账号, 守护进程不会退出
- 目的/目标:在后台周期性执行某些特定的操作
1.2 进程组
多个进程的集合. 所有的进程都有资格创建一个进程组, 也可以加入到某个进程组
如果某些进程处理的操作相同, 可以将他们划分到同一个进程组中, 方便管理.
一个进程组中至少有一个进程
-
进程组长的指定?
- 进程组中的第一个进程就是组长
-
进程组ID如何指定?
- 进程组的组ID (PGID) == 当前组的组长的进程ID(PID)
- 通过
ps ajx
命令可以查看
-
函数
// 获取当前进程所在的进程组的组ID pid_t getpgrp(void); // 获取指定进程所在的进程组的组ID pid_t getpgid(pid_t pid); - pid: 指定的进程的PID // 1. 让pid进程加入到进程组pgid // 2. 如果在加入的过程中发现pgid不存在, 创建 // 创建新进程组组ID == 创建进程组的进程的PID // pid == pgid -> 创建新的进程组 // pid != pgid -> 加入其它进程组 int setpgid(pid_t pid, pid_t pgid); - pid: 要加入或创建进程组的进程 - pgid: 进程组ID
1.3 会话
多个进程组的集合,方便操作系统管理.
创建守护进程,进程必须要变成会话/会长,才可以提升为守护进程
必须是普通进程才可以提升为会话, 如果是进程组组长是没有资格的
进程变成会话(会长)之后, 会脱离原来的终端
- 函数
// 获取进程所属的会话ID
#include <unistd.h>
pid_t getsid(pid_t pid);
返回值:
成功: 会话ID, 失败: -1
// setsid():将一个进程变成会话/会长
// 必须是普通进程才可以提升为会话, 如果是进程组组长是没有资格的
pid_t setsid(void);
- 创建会话注意事项:
- 不能是进程组长
- 创建会话的进程成为新进程组的组长
- 有些linux版本需要root权限执行此操作(ubuntu不需要)
- 创建出的新会话会丢弃原有的控制终端
- 一般步骤:先fork, 父亲死, 儿子执行创建会话操作(setsid)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
pid_t pid = fork();
if(pid > 0)
{
exit(0);
}
// 子进程
setsid();
while(1)
{
sleep(1);
}
return 0;
}
1.4 创建守护进程的步骤
- 1. 创建一个子进程, 父进程默认是进程组的长
fork();
- 2. 杀死父进程, 父进程没有利用价值了
自杀, 他杀
- 3. 将子进程提升为会话--该进程会脱离终端
setsid();
- 4. 修改子进程的工作目录 (不是必须的)
- 需要切换的场景: 进程是在一个U盘中启动的, 如果U盘被卸载, 进程的工作目录就没有了, 进程就不能工作了
- 需要调用的函数:
int chdir(const char *path);
- 5. 修改文件的掩码 (umask) -> 通过掩码控制文件的权限(不是必须的)
mode_t umask(mode_t mask);
- 6. 关闭文件描述符或者重定向 (不是必须的)
- 关闭:
close(0);
close(1);
close(2);
- 重定向到空设备文件: /dev/null
int fd = open("/dev/null",O_RDWR);
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
- 7. 核心操作
测试题1
1. 写一个守护进程, 每隔2s获取一次系统时间, 将这个时间写入到磁盘文件.
创建守护进程- 6步
注册信号捕捉 -> sigaction
设置定时器 -> setitimer
获取系统时间操作
写磁盘
01_test01_daemon_process_signal.c
// 1. 写一个守护进程, 每隔2s获取一次系统时间, 将这个时间写入到磁盘文件.
//
// 创建守护进程- 6步
// 注册信号捕捉 -> sigaction
// 设置定时器 -> setitimer
// 获取系统时间操作
// 写磁盘
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/time.h>
#include <time.h>
//3.2、回调函数
//捕捉到定时器发出的信号之后的处理动作
void callback_writefile(int num)
{
//4.1、获取当前时间
time_t curtime = time(NULL);
//需要将得到的总秒数,转换为本地实际
//方式1:利用函数的返回值,自己拼接出时间
//方式2:使用asctime函数,得到格式化好的时间
struct tm* mytime = localtime(&curtime);
//使用asctime函数,格式化时间结构体
char * strtime = asctime(mytime);
//得到时间字符串
//4.2、写入到文件里面
int fd3 = open("mytesttime.txt",O_WRONLY|O_APPEND|O_CREAT,0664);
write(fd3,strtime,strlen(strtime));
close(fd3);
}
int main()
{
//1、创建守护进程
//1.1、创建子进程
pid_t pid = fork();
//1.2、杀死父线程
if(pid > 0)
{
//父进程
exit(0);
}
//父进程已经被杀死,下面都是子进程的代码
//1.3、将子线程提升为会话
setsid();
//1.4、切换工作目录
chdir("/home/lwh");//改为当前用户的家目录
//1.5、修改文件掩码
umask(0002);//设置的和系统默认是一样的
//1.6、文件重定向
int fd2 = open("/dev/null",O_RDWR);
dup2(fd2,0);
dup2(fd2,1);
dup2(fd2,2);
//1.7、核心操作
//3.1、注册信号捕捉(在信号发出之前)
//因为:定时器发出的信号会杀死当前的进程/线程
struct sigaction mysig;
mysig.sa_flags = 0;
sigemptyset(&mysig.sa_mask);
mysig.sa_handler = callback_writefile;
sigaction(SIGALRM,&mysig,NULL);
//2.1、设置定时器
struct itimerval newtime;
newtime.it_value.tv_sec = 5;//第一次触发的时间间隔
newtime.it_value.tv_usec = 0;
newtime.it_interval.tv_sec = 2;//周期性间隔
newtime.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL,&newtime,NULL);
while(1)
{ //setitimer执行完了以后,进程直接退出了
//也就无法在后台继续运行了
sleep(10000);
//让进程一直睡
//因为信号优先级很高,当捕捉到信号后
//就会醒来去执行回调函数
//处理完了之后,接着会去睡
//如此往复的循环执行
}
return 0;
}
2. 线程
2.1、概念
操作系统会以进程为单位,分配系统资源,所以我们也说,进程是资源分配的最小单位。
线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。
线程是操作系统调度执行的最小单位.
- 每个进程对应一个虚拟地址空间
- 多个线程共用同一个虚拟地址空间
- 子线程和父线程(主线程)共用同一个虚拟地址空间
- 线程是从进程中分出去的
- 虚拟地址空间不会发生复制
- 虚拟地址空间内核中的PCB会被复制一份
- 进程会争抢cup资源、线程也会争抢cpu资源
- 因为:线程被内核当做了进程
- 在进程中创建线程时
- 虚拟地址空间不会发生复制
- –> 占用的系统资源和原来一样多,并没有变多
- 但是虚拟地址空间内核中的PCB会被复制一份
- –>内核认为多了一个进程
- –>程序就可以抢到更多的CPU时间
- 程序执行时间更长,效率就更高
安装线程 man page,命令:
sudo apt-get install manpages-posix-dev
查看线程的LWP号
- ps -Lf
- 这是给内核看的
2.2、线程之间共享和非共享资源
2.2.1.共享资源
-
文件描述符表!!!
-
每种信号的处理动作 -> 信号捕捉到之后的回调函数
-
当前工作目录
-
用户ID和组ID
-
虚拟地址空间的用户区 (.text/.data/.bss/heap/共享库)!!!
除了栈区
2.2.2.不共享资源
-
线程id -> 无符号长整形!!!
每个线程都有自己的线程ID, 这是唯一的
-
处理器现场和栈指针(内核栈)
-
独立的栈空间(用户空间栈)!!!
-
errno变量
-
阻塞信号集
用多线程不要用信号!用信号就不要用多线程!
-
调度优先级 -> 线程的优先级
默认是相同的, 可以设置不同线程的优先级
2.3、创建线程
// 创建子线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数:
- thread: 创建成功之后, 线程的线程ID被传出
- attr: 设置线程的属性, 一般使用默认属性就可以了, 指定为NULL
- start_routine: 函数指针, 指向的函数(回调函数)就是子线程的处理逻辑
- arg: 给回调函数传参
// 程序执行过程中:
- 主线程执行完了, 子线程代码还没有执行完 -> 主线程退出, 子线程被强制杀死
- 主线程没有执行完, 子线程代码执行完了 -> 子线程退出, 主线程继续运行
// 测试代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 子线程处理函数
void* childFunc(void* arg)
{
printf("child thread id = %ld\n", pthread_self());
for(int i=0; i<5; ++i)
{
printf("child_thread i = %d\n", i);
}
return NULL;
}
int main()
{
// 创建子线程
pthread_t ptid;
pthread_create(&ptid, NULL, childFunc, NULL);
// 主线程的处理代码
printf("main thread id = %ld\n", pthread_self());
for(int i=0; i<5; ++i)
{
printf("main_thread i = %d\n", i);
}
while(1)
{
sleep(1);
}
return 0;
}
$ gcc pthread_create.c
/tmp/ccyln1Gw.o: In function `main':
pthread_create.c:(.text+0x7f): undefined reference to `pthread_create'
collect2: error: ld returned 1 exit status
# 错误原因: 编译的时候没有引用对应的线程库, 需要指定使用的线程库 -l pthread
2.4、获取当前线程的线程ID
#include <pthread.h>
pthread_t pthread_self(void);
返回值:pthread_t -- 线程ID的类型, 无符号长整形
2.5、指定使用的线程库
Compile and link with -pthread.
编译和链接的时候需要指定动态库的名字pthread
--> 需要指定使用的线程库 -l pthread
2.6、线程退出
#include <pthread.h>
void pthread_exit(void *retval);
-->使用这个函数退出某个线程, 对其他线程的运行没有影响
使用这个函数使主线程退出
-->子线程不会受到影响(不会被强制杀死了)
使用这个函数使子线程退出
-->主线程不受影响
-->参数:线程退出的时候携带的数据
不携带数据, 参数写NULL
-->比如:exit()退出进程携带的数据
void exit(int status);
#include <pthread.h>
// 子线程处理函数
void* childFunc(void* arg)
{
printf("child thread id = %ld\n", pthread_self());
for(int i=0; i<15; ++i)
{
printf("child i = %d\n", i);
if(i == 10)
{
pthread_exit(NULL);
}
}
return NULL;
}
int main()
{
// 创建子线程
pthread_t ptid;
pthread_create(&ptid, NULL, childFunc, NULL);
// 主线程的处理代码
printf("main thread id = %ld\n", pthread_self());
for(int i=0; i<5; ++i)
{
printf(" i = %d\n", i);
}
// 退出主线程, 对子线程没有影响
pthread_exit(NULL);
return 0;
}
2.7、回收子线程资源
// 回收已经死亡的子线程的内核资源
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
子线程退出之后, 内核中的pcb自己释放不了, 需要由主线程来释放
该函数调用一次, 子线程就被回收一个
这个函数是一个 阻塞函数
一般情况下第二个参数不使用, 写NULL
参数:
- thread: 要回收的线程的线程ID
- retval: 通过这个参数接收pthread_exit( )函数返回的数据
注意事项: pthread_exit( )传递给主/父线程的地址必须是:
- 堆区地址
- 全局数据区地址
- 栈区地址不行, 线程退出的时候, 栈内存就被释放了
#include <pthread.h>
struct Info
{
char name[24];
int age;
};
struct Info info;
// 子线程处理函数
void* childFunc(void* arg)
{
printf("child thread id = %ld\n", pthread_self());
for(int i=0; i<15; ++i)
{
printf("child_thread i = %d\n", i);
if(i == 10)
{
strcpy(info.name, "lilei");
info.age = 24;
pthread_exit(&info);
}
}
return NULL;
}
int main()
{
// 创建子线程
pthread_t ptid;
pthread_create(&ptid, NULL, childFunc, NULL);
// 主线程的处理代码
printf("main thread id = %ld\n", pthread_self());
for(int i=0; i<5; ++i)
{
printf(" i = %d\n", i);
}
// 回收子线程资源
struct Info* pt;
pthread_join(ptid, (void**)&pt);
printf("name: %s, age: %d\n", pt->name, pt->age);
// 退出主线程, 对子线程没有影响
pthread_exit(NULL);
return 0;
}
2.8、线程分离
// 线程分离(父子线程分离)之后, 子线程结束之后, 其内核资源就不需要主线程去释放了
// 是由系统释放(不是子线程自己释放)
// 线程分离之后,子线程资源由系统释放,不需要我们管
// 分离之后在主线程中不需要调用pthread_join函数
#include <pthread.h>
int pthread_detach(pthread_t thread);
参数
thread: 子线程ID
2.9、线程取消 -> 杀死子线程
// 在主线程中调用线程取消函数, 杀死子线程
// 不是这个函数一调用子线程马上死, 子线程运行的时候遇到一个取消点才死
// 取消点: 代码执行的时候需要从用户区 -> 内核区进行一个切换 (进行一次系统调用)
#include <pthread.h>
int pthread_cancel(pthread_t thread);
// 参数: 要杀死的线程的线程ID
2.10、比较线程ID是否相同
// 线程ID类型:长整形
// 在其他非linux平台, 这个类型有可能不是长整形, 因此需要使用该函数进行比较
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
返回值:
相同: 非0
不同: 返回值为0
3. 线程属性
1、线程分离(相当于调用了pthread_detach)
-
需要属性的变量 -> 对应一块内存
-
首先要申请一块内存, 并且初始化, 保存属性信息
-
当属性信息对应的变量使用完毕, 需要释放
//1、创建--线程属性变量 pthread_attr_t attr;// pthread_attr_t:线程属性类型 // 2、初始化--线程属性变量 #include <pthread.h> int pthread_attr_init(pthread_attr_t *attr); // 3、设置属性:实现线程分离 int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); - detachstate: PTHREAD_CREATE_DETACHED: 设置线程分离 PTHREAD_CREATE_JOINABLE: 设置父子线程不分离 //4、把线程属性设置到线程里面去 #include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void * (*start_routine) (void *),void *arg); //5、释放线程属性变量(设置进去之后,即可释放/销毁) int pthread_attr_destroy(pthread_attr_t *attr);
测试题2
2. 使用多线程测试线程之间是否共享全局变量
使用n个线程数数, 每个线程数1000个数, 看最终的结果
// 需要加锁
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#define MAX 100
int number = 0;
pthread_mutex_t mutex;
void *childFunc1(void *arg)
{
for (int i = 0; i < MAX; ++i)
{
pthread_mutex_lock(&mutex);
number++;
usleep(10); //睡10u秒
number++;
--number;
printf("Thread 1 .tid = %lu,number = %d\n", pthread_self(), number);
pthread_mutex_unlock(&mutex);
}
}
void *childFunc2(void *arg)
{
for (int i = 0; i < MAX; ++i)
{
pthread_mutex_lock(&mutex);
number++;
usleep(10); //睡10u秒
number++;
--number;
printf("Thread 2 .tid = %lu,number = %d\n", pthread_self(), number);
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_t ptid1, ptid2;
pthread_create(&ptid1, NULL, childFunc1, NULL);
pthread_create(&ptid2, NULL, childFunc2, NULL);
printf("main_thread tid : %ld\n", pthread_self());
for (int i = 0; i < 5; i++)
{
printf("main_thread i = %d\n", i);
}
pthread_join(ptid1, NULL);
pthread_join(ptid2, NULL);
pthread_exit(NULL);
return 0;
}