//Linux下C语言编程常用头文件
//标准空间
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//字符串处理
#include <string.h>
//文件处理
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//线程
#include <pthread.h>
//信号
#include <signal.h>
//套接字网络
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
一、进程基础
操作系统基础的执行单元,调度单位
静态数据:只占用磁盘空间,不消耗其他资源
动态数据:磁盘 内存 CPU
1. 编译器将源码编译成一个可执行文件.exe/.elf
2. 运行后系统生成一个同名的进程
程序是进程的静态表现,进程是程序的动态表现
3. 进程在创建时,要分配虚拟内存(线程不分配内存)
进程是最小的分配资源单位,也是调度单位
因为每个进程创建时分配内存资源,所以被成为分配资源单位
进程没有实体,由复杂的逻辑关系构成
进程的生存环境,形态:
PCB 进程控制块,是一个300多个成员的结构体,记录了详细的进程信息
虚拟内存地址通过内存映射访问真正的物理内存地址
进程中虚拟地址不同,但是指向相同的物理地址,实现共享内存【内核空间】
不同的进程内存独立【用户空间】(以防互相影响,内核空间只有系统拥有权限,不怕被修改)
程序优化:系统占用以及系统开销(CPU 内存)
内存基本单位:Page(页) 1Page=4KB=4096Bytes
malloc(8192) ——> 一次分配两页内存
malloc(5000) ——>分配两页内存,对剩余内存空间进行限制访问
malloc(3192) ——>检查是否使用完毕,并解除限制
内存的权限:PROT_READ 只读 PROT_WRITE 只写 PROT_EXEC 执行 PROT_NONE 无权限
虚拟内存:32位系统,三级间接寻页:1024*1024*1024=GB
63位系统,四级间接寻页,1024*1024*1024*1024=TB
二、进程状态以及状态转换
进程的五种常态
就绪态Ready:进程已准备除处理器外所需资源,等待cpu资源
运行态Running:占用cpu,正在执行指令
阻塞态Blocked:由于进程等待某种条件(如I/O操作或进程同步)被暂停(释放cpu),在条件满足之前无法继续执行。
挂起态Suspend:由于用户和系统的需要被暂停(释放cpu),把进程从内存转到外存
终止态Terminated:进程退出,释放进程内存资源
阻塞态和挂起态的区别?
1. 阻塞态可以被中断,在等待的资源得到满足后,进入就绪状态;挂起态无法被中断,只有将其挂起的对象唤醒后,才可以让其继续
2. 阻塞态释放了cpu,但是阻塞的进程还在内存中;挂起的进程通过swp交换到外存中(磁盘空间)
3. 阻塞态发生在进程等待资源时,挂起是由于用户和系统需要
任意状态都可以切换为终止态
Linux独有的状态
僵尸态Zombie:子进程退出结束,但是PCB残留,导致内存泄漏,叫做僵尸进程
孤儿态Orphan:父进程先挂掉,子进程会被1号进程领养,我们可以理解为操作系统,或者说父进程先结束,子进程必须被操作系统领养,因为无主进程,一直僵尸浪费资源,我们将这种进程称之为孤儿进程。 还会将自己变成后台进程。
分时复用原则(时间片)
多进程共享使用CPU,每个进程按时间片(10us)使用,而后快速切换,既可以让多个进程共享使用,以提高cpu使用率
单任务操作系统(软盘) ——> 多任务操作系统(充分共享使用硬件)
大多数系统的程序都是并发执行的(快速交替执行)
保存与恢复处理器现场
CPU原件:控制器、寄存器(Eax,Ebx,Edx,Eflag....)、编码器、译码器、运算器
寄存器:
内存:快速缓存数据及计算过程
每个进程都有自己独立的PCB,每个PCB都有一个内核栈指针kernel_ptr,指向自己的内核栈
多任务基准:分时+保存与恢复
什么是线程?什么是进程?
进程或者线程就是寄存器(CPU的访问权)和栈(内核栈)
进程、线程、纤程:调度单位
协程、管程:管理单元
超线程:硬件,模拟多核
二、进程源语
2.1 进程源语函数
系统支持进程开发提供的一系列API函数接口
pid_t pid;进程id类型
pid_t pid=getpid(void);//调用获取进程pid
2.1.1 fork() 创建子进程
pid_t pid=fork(); //创建子进程,执行一次创建一个进程
子进程与父进程pid连续,父进程最小
父进程调用fork,系统创建子进程,子进程从fork之后执行
关于子进程继承的问题?
1. 拷贝继承
2. 区分父子进程代码段,实现父子执行不同的代码任务
使用fork的返回值pid进行父子进程的区分
子进程不允许踏出工作区
1. 永久阻塞 2. 进程退出
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
//parent start
int main(){
pid_t pid;
pid=fork(); //创建子进程
if(pid>0){
printf("parent pid %d, Running...\n",getpid());
}
else if(pid==0){
//child start
printf("child pid %d, Running...\n",getpid());
while(1)
sleep(1);
//child end
}
else if(pid==-1){
perror("fork call failed");
exit(0);
}
printf("self id %d\n",getpid());
while(1)
sleep(1);
return 0;
}
//parent end
windows的printf自带刷新缓冲区
linux下\n刷新缓冲区
3. 创建多进程
循环创建多进程
int i;
for(int i=0;i<3;i++){
pid=fork();
if(pid==0)
break;
}
4. 子进程执行fork,并且得到返回值0
两个进程执行同一个fork()
栈帧
不同的函数有各自的函数栈帧地址,其他位置无法访问函数内部资源
父子进程可以共享同一个函数的栈帧地址,实现调用同一个函数,执行一部分
gdb可以实现栈帧的跳转
第一版 fork,拷贝继承版本
如果子进程不需要拷贝父进程数据,而是自行获取用户空间,那么父进程的拷贝流程和开销变成无意义的系统开销
第二版 vfork,无拷贝过程,但是不能直接使用
通过vfork创建的子进程没有用户空间,需要用户自行生成用户层,vfork必须结合execl函数使用
第三版 fork,读共享写复制
读时共享,虚拟地址不同,物理地址相同
父子进程写都会导致子进程复制共享内存数据
例题:
2*9+1=19个子进程
2*10-1=19
2.1.2 execl() 进程重载
系统名都是程序,执行命令时创建进程,完成特定功能
重载只替换进程工作数据,与PCB进程信息无关
execl("程序路径",argv[0]/*文件名|命令*/,argv[1]/*参数*/,NULL/*哨兵,结束*/);
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
pid_t pid;
pid=fork();
if(pid>0){
printf("parent pid %d\n",getpid());
while(1)
sleep(1);
}
else if(pid==0){
printf("child pid %d\n",getpid());
printf("child execl...\n");
execl("/usr/bin/firefox","firefox","www.bilibili.com",NULL);
printf("execl failed\n");
exit(0);
}
else if(pid<0){
printf("fork failed\n");
exit(0);
}
return 0;
}
使用execl
1. vfork创建子进程无法直接使用,必须重载才可以使用
2. 重载方便程序的功能扩展,很多现有的命令在进程中可以直接使用
3. 动态迭代
在程序不下线的情况下,动态更新功能
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
pid_t pid;
printf("MyWeb Vesion 1.o Running...\n");
while(1){
pid=fork();
if(pid==0)
break;
sleep(5);
}
if(pid==0)
execl("./MOD/mod1","mod1",NULL);
return 0;
}
此时只需要修改mod1.c文件内容并重新编译,就可以在线修改主程序的功能
2.1.3 wait() waitpid() 回收函数
僵尸态Zombie:子进程退出结束,但是PCB残留,导致内存泄漏,叫做僵尸进程
_EXIT(0) 结束进程时,完全释放了用户空间,释放了部分内核空间,只有PCB残留——内存泄漏
危害:1. 内存泄漏 2. 僵尸进程残留PCB无法给新进程使用,影响进程创建数量
系统为何要保留PCB?
僵尸进程需要父进程进行手动回收、检验子进程是否正常退出
关于ps -aux进程状态stat的中Ss、S<l、Ssl、SLl、SNl、R、R+的解释_ps aux 进程 tl sl-CSDN博客
int stat;
pid_t zpid=wait(NULL); //表示只回收pcb不进行回收的验证操作,返回值为僵尸进程pid
pid_t zpid=wait(&stat); //返回子进程状态
wait() 失败原因:没有子进程立即失败
wait() 是阻塞函数,阻塞回收,如果子进程未结束,无需回收,wait阻塞等待
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
pid_t pid;
pid_t zpid;
pid=fork();
if(pid>0){
zpid=wait(NULL);
printf("parent pid %d, child pid %d, zpid %d\n",getpid(),pid,zpid);
while(1)
sleep(1);
}
else if(pid==0){
printf("child alive 10s\n");
sleep(10);
exit(0);
}
else {
perror("fork failed");
exit(0);
}
return 0;
}
只有父进程可以回收子进程,其他进程无法代替
init 1号进程没有父进程,称作根进程
子进程先于父进程死亡,必然会成为僵尸;父进程先于子进程死亡,子进程必然会成为孤儿
waitpid(pid_t pid,int* state,WNOHANG/*非阻塞关键字*/);
//返回值:>0 成功返回| -1 失败| =0 非阻塞返回
pid==-1 回收任意子进程
pid>0 回收指定子进程(进程号)
pid==0 同组回收
pid<-1 跨组回收(-进程组号)
waipid() 支持非阻塞回收,相比wait() 更加灵活。在等待回收时,父进程可以执行自己的任务和代码。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
pid_t pid;
pid_t zpid;
pid=fork();
if(pid>0){
while((zpid=waitpid(-1,NULL,WNOHANG))!=-1){
if(zpid>0)
printf("parent pid %d, child pid %d, zpid %d\n",getpid(),pid,zpid);
else if(zpid==0){
printf("parent exec jobs\n");
sleep(1);
}
}
while(1)
sleep(1);
}
else if(pid==0){
printf("child alive 10s\n");
sleep(10);
exit(0);
}
else {
perror("fork failed");
exit(0);
}
return 0;
}
2.2 多进程拷贝
单进程的CPU使用资源受限,可以采用多进程模型,获取更多的时间片,加快任务完成速度,提高效率
两种常见的多进程:多进程任务较为复杂;多进程任务单一
串行:逐次完成任务
并行:多核CPU且每一个工作进程都能加载到对应的处理核心上【由CPU决定,不可控】
并发:单个处理器,逻辑上同步执行【有并行的概率】多进程并发、多线程并发
程序的执行效率由cpu的占用及使用频率决定,占用越多效率越高
单进程模型因阻塞或其他原因放弃本轮cpu使用,但是多进程或者多进程模型,根据就近原则,放弃的进程可以交替给相邻的进程,让任务继续
四、进程间通信(IPC)
进程间通信技术,便于多进程模型中,进程间共享或传递数据的一种手段(有多种方式)
1、管道(匿名管道、命名管道)
2、socket套接字
3、消息队列(Posix,SystemV)
4、signal信号
5、内存映射(mmap)
用户层无法通信(进程独占资源),内核层是共享内存。绝大多数的进程间通信手段,都是基于内核层实现的。
4.1 匿名管道
特征:
1、是一种传输介质
2、具备方向性(传输方向)
3、暂时存储数据
4.1.1 创建管道
//系统创建管道
pipe (int fds[2]); //成功0,失败-1
弊端:匿名管道只支持亲元进程通信,如果进程不相关,无法使用
4.1.2 通信方向
一般采取单工传输(半双工使用,管理和控制复杂)
传输方式:
单工传输:任意时刻,非读即写,无法切换(管道、消息队列)
半双工传输:一个时刻,非读即写,不同时刻可以切换(可调节单工)
双工传输:任意时刻,同时读写(socket)
管道的引用计数=0:系统释放管道空间
不用的一端使用close()关闭
close(fds); //引用计数-1
shutdown(); //清0引用计数
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
#define msg "hi son"
//单工传输,匿名管道,父写子读
int main(){
pid_t pid;
int fds[2];
pipe(fds);//管道创建
pid=fork();
if(pid>0){
close(fds[0]);//关闭读端
write(fds[1],msg,strlen(msg));//发送数据
printf("parent pid %d, write sucess\n",getpid());
close(fds[1]);//计数清零,释放管道
wait(NULL);
}
else if(pid==0){
char buf[4096];//缓存管道4k
bzero(buf,sizeof(buf));
close(fds[1]);//关闭写端
read(fds[0],buf,sizeof(buf));
printf("child read msg=%s\n",buf);
close(fds[0]);//计数清零,释放管道
}
else{
perror("fork failed");
exit(0);
}
return 0;
}
ubuntu 16.04往后版本匿名管道大小为4K,之前的版本为64K
匿名管道使用时四种特殊情况(具有普遍意义):
1、写阻塞:写端写管道,读端未读,管道写满后,系统阻塞写端
2、读阻塞:写端未写,管道为空,读端读管道,系统阻塞读端
3、写端关闭,读端读取管道剩余数据,管道空再次读,返回0
4、读端关闭,写端尝试写数据,系统向写端进程发送信号SIGPIPE,杀死写端进程
例:客户端向服务端发送一条请求数据,客户端异常关闭,服务端读取请求,处理请求,向客户端发送响应数据,结果服务端异常退出,为什么?
因为读端关闭,服务器写端尝试写数据时被信号杀死了
使用send(sock,buffer,len,MSG_NOSIGNAL); 在发送数据时忽略信号
send(sock,buffer,len,MSG_NOSIGNAL); //忽略信号
recv(sock,buffer,MSG_DONTWAIT); //非阻塞读
4.2 命名管道
管道文件:不是常规文件,是设备文件。
并不是采用文件通信,而是使用文件的操作方式,来访问使用管道(指向管道缓冲区的指针)
向管道文件读写的数据,都会重定向到管道缓冲区中
管道文件与管道缓冲区绑定,创建文件则创建缓冲区,删除文件则释放缓冲区
4.2.1 创建管道文件
mkfifio 管道文件名
mkfifio(char* name,0664); //函数常见
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
//文件相关头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define msg "pipe msg"
//写端
int main(){
int wfd;
mkfifo("串休",0664); //创建管道
wfd=open("串休",O_WRONLY);
write(wfd,msg,strlen(msg));
printf("write process %d, sucess\n",getpid());
close(wfd);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
//文件相关头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//读端
int main(){
int rfd;
char buffer[4096];
bzero(buffer,sizeof(buffer));
rfd=open("串休",O_RDONLY);
read(rfd,buffer,sizeof(buffer));
printf("Read process %d, %s\n",getpid(),buffer);
close(rfd);
unlink("串休"); //删除管道(硬链接计数器-1)
return 0;
}
发现写端在open阻塞。
管道文件的访问限制(权限):管道文件的访问要求,必须满足两种权限(O_WRONLY && O_RDONLY)才能成功打开管道,如果只有一种则需要等待另一种(open阻塞)
只要满足权限即可打开使用管道,没有进程数量限制(O_RDWR读写打开)
命名管道使用特殊情况(匿名管道的特殊情况也适用):
1、访问管道文件时必须满足读写权限,才可以打开管道
2、一个进程中包含多个读序列(多个线程),阻塞只对第一个读序列生效,其他读序列自动设置为非阻塞
避免了无意义的阻塞开销
3、管道的原子访问与非原子访问。
原子访问:只要用户写的数据量<=管道大小,则为原子写。如果写入量超过余量,系统会挂起写端,直到读取后余量满足才唤醒。
优点:保证数据传输的完整性,避免多个数据包掺杂在一起
缺点:传输效率低,因为写端会经常被挂起
非原子访问:写数量>管道大小,则为非原子。只要管道余量不为0,那么写端可以随时写任意数据量到管道中
优点:较高的通信传输速度
缺点:数据会被拆分切割,读端必须对读的数据进行验证,否则无法使用
数据包一般是定量的(对数据进行封装,协议、消息),一般不会是变量
4.3 mmap内存映射
通过内存映射手段完成通信
映射文件MapFile,把文件内容通过映射的方式写道内存中(读共享:虚拟不同,物理相同)
映射函数
void* ptr=mmap(NULL/*映射内存地址,参数为NULL时,系统在库空间申请内存;不为空时,在堆空间*/,
size/*映射内存大小,一般为映射文件大小(映射文件必须有大小)*/,
PROT_READ|PROT_WRITE/*内存映射权限*/,
MAP_SHARED|MAP_PRIVATE/*映射方式:共享映射|私有映射*/,
int fd/*映射文件的文件描述符*/,
0/*映射偏移量*/);
//成功返回映射内存的地址;失败返回MAP_FALIED关键字
munmap(ptr,size); //释放映射内存
- 私有映射:拷贝映射,将文件中的数据拷贝一份到进程的映射内存。拷贝的多份数据无关联,修改任意一端数据不会影响另一端。
- 共享映射:同步映射,SYNC同步机制。修改任意一份映射数据(内存/文件)都会实时同步,保持一致性
文件为空,但是有大小?
从小到大截断——ftruncate(int fd,size); //使用截断的方可以扩展空文件
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/fcntl.h>
#include<sys/mman.h>
//映射,通过同步机制,修改文件
int main(){
int* ptr=NULL;//指针类型决定操作的长度
int fd=open("MapFile",O_RDWR);//读写打开映射文件
int size=lseek(fd,0,SEEK_END);//计算文件大小
if((ptr=mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0))==MAP_FAILED)//内存映射
{
perror("mmap failed");
exit(0);
}
close(fd);
printf("映射成功,文件数据:%s",(char*)ptr);
ptr[0]=0x34333231;
printf("修改成功\n");
return 0;
}
映射通信
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/mman.h>
#include<sys/fcntl.h>
//mmap 写端
typedef struct{
int id;
char name[20];
}msg_t;
int main(){
int fd=open("MAP",O_RDWR|O_CREAT,0664);
//截断空文件
ftruncate(fd,sizeof(msg_t));//将文件截断为结构体大小
msg_t* ptr=NULL;
if((ptr=mmap(NULL,sizeof(msg_t),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0))==MAP_FAILED)
perror("mmap failed");
close(fd);
ptr->id=0;
bzero(ptr->name,20);
while(1){
++(ptr->id);
sprintf(ptr->name,"testName %d",ptr->id);//写端修改数据
sleep(1);
}
munmap(ptr,sizeof(msg_t));
return 0;
}
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/mman.h>
#include<sys/fcntl.h>
//mmap 读端
typedef struct{
int id;
char name[20];
}msg_t;
int main(){
int fd=open("MAP",O_RDWR|O_CREAT,0664);
msg_t* ptr=NULL;
if((ptr=mmap(NULL,sizeof(msg_t),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0))==MAP_FAILED)
perror("mmap failed");
close(fd);
while(1){
printf("%s\n",ptr->name);//读端输出数据
sleep(1);
}
munmap(ptr,sizeof(msg_t));
return 0;
}
映射失败的原因?
1.权限不足,打开文件的权限<=映射权限
2.映射文件异常,映射大小为0,导致无法申请内存
为何会出现总线错误SIGBUS?
映射内存的大小超过了文件大小,则会发生越界/溢出错误
有些情况下,mmap可以代替read
mmap比较适合读取处理文件(适合大文件数据)?
读取文件的操作上,mmap映射手段效率更高,开销更小,相比read减少拷贝开销
大数据文件可以通过mmap进行偏移映射,分段处理
零拷贝技术:在读写时,可以减少拷贝开销,以及可以减少上下层级转换的次数
SendFile
应用于socket套接字、信号等
五、进程关系
5.1 亲缘关系
强亲缘(父子关系):子进程整个生命周期,父进程全程参与(创建、克隆、回收)
弱亲缘(多级亲缘关系):多级的亲缘关系,只需要继承即可
5.2 孤儿进程
孤儿进程:父进程先于子进程退出(异常退出),子进程变为孤儿进程,引起隐患与危害
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
pid_t pid;
pid=fork();
if(pid>0){
printf("parent pid %d\n",getpid());
sleep(5);
exit(0);
}
else if(pid==0){
while(1){
printf("child pid %d, ppid %d\n",getpid(),getppid());
sleep(1);
}
}
else{
perror("fork failed");
exit(0);
}
return 0;
}
创建出孤儿进程发现其被托管,与当前终端Bash没有亲缘关系
注意此时进程不是前台进程,而是后台进程,需要使用ps aux查看进程号,然后kill -9 进程号杀死
孤儿进程的预防与处理
1.提高父进程健壮性
2.检测与处理孤儿
(1)利用匿名管道特性,读进程关闭,写进程被杀死,子进程为写端,定时向管道写数据。如果父进程退出,子进程被SIG_PIPE信号杀死
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<string.h>
#include<errno.h>
#include<fcntl.h>
int main(){
//管道检测孤儿进程并处理
pid_t pid;
int i=0;
int fds[2];
pipe(fds);
for(i;i<2;i++){
pid=fork();
if(pid==0)
break;
}
if(pid>0){
close(fds[0]);
printf("parent pid %d\n",getpid());
while(1)
sleep(1);
close(fds[1]);
}
else if(pid==0){
int len;
int flag;
char buffer[10];
bzero(buffer,sizeof(buffer));
close(fds[1]);
//非阻塞读管道
flag=fcntl(fds[0],F_GETFL);
flag|=O_NONBLOCK;
fcntl(fds[0],F_SETFL,flag);
while(1){
printf("child pid %d\n",getpid());
if((len=read(fds[0],buffer,sizeof(buffer)))==-1){
if(errno==EAGAIN){
//非阻塞读
}
else{
perror("read call failed");
exit(0);
}
}
if(len==0){
printf("check parent exit, child %d exit\n",getpid());
exit(0);
}
sleep(1);
}
close(fds[1]);
}
else{
perror("fork failed");
exit(0);
}
return 0;
}
(2)子进程通过信号检测父进程是否退出。如果是,子进程退出,避免孤儿
kill(getppid(),0);
5.3 进程组 Process_Group
管理多进程的结构
脱离组关系:
1.成为组长
2.
5.4 进程会话关系——守护进程
ps ajx 查看进程关系
终端进程管理关系,用于管理若干终端子进程
setsid(); //当前进程创建会话
getsid(getpid()); //获取当前进程会话id
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
pid_t pid;
pid=fork();
if(pid>0){
printf("parent pid %d, ppid %d\n",getpid(),getppid());
while(1);
}
else if(pid==0){
printf("child pid %d, ppid %d\n",getpid(),getppid());
//创建新组
setpgid(getpid(),getpid());
//getsid(getpid());
printf("change group sucess, pgid %d\n",getpgrp());
while(1)
sleep(1);
}
else{
perror("fork failed");
exit(0);
}
return 0;
}
某个进程脱离终端控制,不受终端控制,终端结束与进程无关
普通进程生命周期:随使用者持续,开启与关闭
用户自定义进程,拥有丰富的功能及任务
工作模式:在允许的情况下尽可能占用系统资源,完成软件功能给用户好的使用体验
守护进程生命周期:开机自启动,关机结束
服务型任务:为某个主要任务提供支持,功能支持或数据支持
工作模式:低开销,间隔执行,条件触发,定时触发
脱离控制终端
实现后台服务器程序(daemon process)
步骤:
开机启动
1.父进程创建子进程,父进程退出
2.子进程创建新会话(守护进程)
3.关闭无用的描述符
守护进程均为后台服务程序,不占用前台。
STDOUT_FILENO、STDIN_FILENO、STDERR_FILENO
关闭标准输出和标准输入,保留标准出错。此时发现perror仍会在终端上输出
dup复制文件描述符/dup2文件描述符重定向
需要使用dup2技术将错误信息重定向到文件中
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
int main(){
close(STDOUT_FILENO);
close(STDIN_FILENO);
int fd,efd;
efd=open("ERROR",O_RDWR);
dup2(efd,STDERR_FILENO);
if((fd=open("noexits",O_RDWR))==-1)
perror("open failed");
return 0;
}
4.修改进程工作目录,改为根目录
进程的默认工作目录,为程序所在位置,如果使用./为当前位置
如果进程无回访,那么没有对程序文件的强制依赖
如果不修改目录,会导致挂载的u盘(程序文件)被占用,无法卸载
5.修改进程umask掩码,改为0002,避免不同设备中umask不一致导致的权限异常
初始权限与umask位运算
6.执行守护进程的核心任务,低开销
7.守护进程退出处理
例:每间隔3s向time.log文件中写入系统时间
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<time.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
void daemon_work(){
//守护进程任务,低开销执行
int fd;
time_t tp;
char tbuf[1024];
bzero(tbuf,sizeof(tbuf));
if((fd=open("time.log",O_RDWR|O_CREAT,0664))==-1)
perror("open failed");
while(1){
tp=time(NULL);//返回种子
ctime_r(&tp,tbuf);
write(fd,tbuf,strlen(tbuf));
bzero(tbuf,sizeof(tbuf));
sleep(3);
}
}
void Create_daemon(){
//守护进程创建接口
pid_t pid;
pid=fork();
if(pid>0){
//1.父进程创建子进程,父进程退出
exit(0);
}
else if(pid==0){
//2.创建新会话,脱离控制终端
setsid();
//3.关闭无用描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
int efd;
dup2(efd,STDERR_FILENO);
//4.修改进程工作目录,注意权限
chdir("./");
//5.修改umask
umask(0002);
//6.执行任务
daemon_work();
//7.退出处理,释放资源
}
else{
perror("fork call failed");
exit(0);
}
}
int main(){
//启动接口
Create_daemon();
return 0;
}
Linux实现进程的开机自启动
Shell脚本(命令解释器),只需要执行权限,不需要进行编译(Python的动态编译与其类似类似)
1.创建脚本文件,后缀sh可省
2.首行声明sh版本 #!/bin/bash
3.编写命令
1 #!/bin/bash
2 ### BEGIN INIT INFO
3 # Provides: daemon
4 # Required-Start: $remote_fs $syslog
5 # Required-Stop: $remote_fs $syslog
6 # X-Start-Before: kdm gdm3 xdm lightdm
7 # X-Stop-After: kdm gdm3 xdm lightdm
8 # Default-Start: 2 3 4 5
9 # Default-Stop:
10 # Description: Start Daemon service
11 ### END INIT INFO
12
13
14 cd "/home/colin/colin/process"
15
16 ./daemon
4.未脚本文件赋予执行权限
sudo chmod 0775 shellname
5.在脚本中加入启动块信息
显示行号:set nu
隐藏行号:set nonu
从init.d中复制一份启动块信息即可
6.将自定义脚本移动到/etc/init.d目录
7.将脚本加入开启启动队列
sudo update-rc.d shellname start 启动顺序 启动级别.
sudo update-rc.d shellname remove