文章目录
进程
基本概念
程序:指编译好的二进制文件,在磁盘上,占用磁盘空间,是一个静态的概念
进程:一个启动的程序,进程占用的是系统资源,如内存、CPU,是一个动态的概念
并发:在一个时间段内,在同一个CPU上,运行多个程序。抢占时间片,同一个时间片上只有一个进程运行
并行:一个时间片上运行着两个或两个以上的进程(必须多个CPU或多核)
PCB:进程控制块,每个进程在内核中都有一个进程控制块来维护进程相关的信息,linux上是task_struct结构体(/usr/src/linux-headers-x.x.x-xx/include/linux/sched.h
)
- 进程ID
- 进程状态:运行、挂起等
- 进程切换时需要保存和恢复的寄存器
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 文件描述符表
- 信号
- 进程可以使用的资源上线:
ulimit -a
进程状态切换
处于就绪态的进程,有执行资格,但是没有CPU时间片
处于挂起态的进程,既没有执行资格,也没有CPU时间片
从挂起态不能直接回到运行态,必须先回到就绪态;
ps/kill命令
ps
——查看进程
ps -aux
ps -ajx
ps -ef
kill
kill -l
:查看系统有哪些信号kill -9 pid
:杀死某个进程
fork创建进程
fork函数
pid_t fork(void);
return:
父进程返回的是子进程的PID(大于0)
子进程返回的是0
注意:并不是一个函数返回两个值,而是父子进程各自返回一个值
由于子进程是父进程fork出来的,故子进程的用户区和父进程的用户区完全一样
所以fork之前的代码,子进程也有,但子进程不会执行,因为子进程的程序PC指针同父进程一样已经移动了
内核区不完全一样,最起码进程ID就不一样
父子进程谁先抢到CPU资源谁先执行,并没有严格的执行顺序
在执行程序时出现下图情况,一定是父进程先退出,子进程后退出。——原因是:父子进程共享文件描述符表,终端是标准输出文件也被共享。在父进程执行结束后回到shell,终端并没有被真正关闭,此时子进程也在用终端,但当前shell不知道父进程里面fork了子进程,所以子进程还在使用终端,使用结束后也会到终端才出现下面情况。
循环创建子进程
下面程序共创建了多少个子进程?
int main()
{
for(int i=0; i<3; i++)
{
pid_t pid = fork();
if(pid==0)
printf("child\n");
else if(pid>0)
printf("parent\n");
}
sleep(10);
return 0;
}
进程链:父进程退出循环,而子进程继续forkif (pid>0) break;
进程扇:子进程退出循环,而父进程继续forkif(pid==0) break;
父子进程变量讨论
linux会采用“写时复制,读时共享”,如下面函数的执行结果
int g_v=30;
int main()
{
int a_v=30;
static int s_v=30;
pid_t pid;
pid=fork();
if(pid==0)
{
g_v=40;a_v=40;s_v=40;
printf("child:\n");
printf("g_v: %p---%d, a_v: %p---%d, s_v: %p---%d\n", &g_v, g_v, &a_v, a_v, &s_v, s_v);
}
else if(pid>0)
{
g_v=50;a_v=50;s_v=50;
printf("parent:\n");
printf("g_v: %p---%d, a_v: %p---%d, s_v: %p---%d\n", &g_v, g_v, &a_v, a_v, &s_v, s_v);
}
sleep(5);
return 0;
}
parent:
g_v: 0x55d1d7e55010---50, a_v: 0x7fffedd0c7e0---50, s_v: 0x55d1d7e55014---50
child:
g_v: 0x55d1d7e55010---40, a_v: 0x7fffedd0c7e0---40, s_v: 0x55d1d7e55014---40
由代码执行结果可以看到,父子进程有相同的虚拟地址空间,但是各自的物理地址空间不一样。
原因如下:系统为每个进程都是分配4G的虚拟内存,前面说过,子进程继承了父进程的用户空间,所以它们的变量虚拟地址一样,但映射到的物理内存会发生不同——写时复制,读时共享
强化练习:
执行下面的程序后,s.txt文件的内容是什么?
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
FILE* fp = fopen("s.txt", "w");
char* s="hello world";
fprintf(fp, "s: %s, pid: %d\n", s, getpid());
pid_t pid=fork();
fprintf(fp, "s: %s, pid: %d\n", s, getpid());
return 0;
}
答案:打开文件是在fork之前,所以父子进程共享了文件,父进程关闭文件,文件并没有被真正关闭。执行fork之前,父进程把字符串会写入到对应的读写缓冲中,fork时,子进程也复制了缓冲区内容,所以父子进程缓冲区都有一样的东西,fork之后,会继续在各自的缓冲区写入内容,最后在写入到文件中
s: hello world, pid: 5782
s: hello world, pid: 5782
s: hello world, pid: 5782
s: hello world, pid: 5783
可以在fork之前先刷新一下缓存,然后在fork,这样子进程的缓冲区就是干净的。
int main()
{
FILE* fp = fopen("t.txt", "w");
char* s="hello world";
fprintf(fp, "s: %s, pid: %d\n", s, getpid());
fflush(fp);
pid_t pid=fork();
fprintf(fp, "s: %s, pid: %d\n", s, getpid());
return 0;
}
s: hello world, pid: 5866
s: hello world, pid: 5866
s: hello world, pid: 5867
exec函数族
如果想在一个进程内部执行执行系统命令或者应用程序,优先想到如下方式:先fork,然后在子进程里面执行execl拉起可执行程序或者命令。
调用execl函数以后,子进程的代码段、数据段、堆和栈会被新的可执行程序或者命令的代码段替换,但子进程的地址空间、PID没有变化。
- execl一般用于执行用户自定义的应用程序;
- execlp一般用于执行系统命令
execl
int execl(const char *path, const char* arg, ... , NULL);
param:
path: 要执行程序的路径
arg: 占位符,通常写要执行程序的名字
arg后: 程序需要的参数
return:
成功:不返回,不在执行execl后面的代码
失败:继续执行execl后的代码,并设置errno
execlp
int execlp(const char *file, const char* arg, ... , NULL);
param:
file: 执行命令的名字,根据PATH环境变量来搜索命令
arg: 占位符,通常写要执行程序的名字
arg后: 程序需要的参数
return:
成功:不返回,不在执行execlp后面的代码
失败:继续执行execlp后的代码,并设置errno
进程回收
当一个进程退出后,进程能够回收自己的用户区资源,但是不能回收内核空间的PCB资源,必须由它的父进程调用wait或者waitpid函数完成对子进程的回收,避免造成系统资源浪费。
孤儿进程
父进程先退出,但子进程还活着,这个进程就成了孤儿进程。孤儿进程会被init进程领养,init进程成为了孤儿进程的父进程,当孤儿进程退出后,由init进程完成对孤儿进程的回收。
僵尸进程
子进程先退出,父进程没有完成对子进程的回收,此时子进程变成僵尸进程
不能使用kill -9
杀死进程,原因是僵尸进程是一个死掉的进程,应该杀死僵尸进程的父进程来回收僵尸进程。原因是:杀死其父进程,可以让init立刻领养僵尸进程,最后由init进程杀死僵尸进程。
wait/waitpid函数
作用:
- 回收子进程函数
- 阻塞并等待子进程退出
- 回收子进程的资源
- 获取子进程的结束状态
调用一次wait或者waitpid函数只能回收一个子进程
- wait
pid_t wait(int *status);
param:
status: 出参,子进程的退出状态
return:
成功:清理掉的子进程PID
失败:返回-1,表示没有子进程
- waitpid
pid_t waitpid(pid_t pid, int *status, int options);
param:
pid: pid=-1, 等待任一子进程
pid>0, 等待指定进程的PID
status: 出参,子进程的退出状态
options: 0表示函数阻塞
WNOHANG表示非阻塞,如果此次没有回收,除非循环调用,不然后面父进程也不会回收了
return:
>0:表示清理掉的子进程PID
-1:表示没有子进程
=0:且options为WNOHANG,表示子进程正在运行
综合练习
父进程创建三个子进程,并且父进程回收三个子进程,并打印子进程的退出状态
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
int main()
{
int i;
pid_t pid;
for(i=0; i<3; i++)
{
pid=fork();
if(pid>0)
{
printf("parent: pid=[%d]\n", getpid());
}
else if(pid==0)
{
printf("child: pid=[%d]\n", getpid());
break;
}
}
if(i==3)
{
//父进程
printf("------pid=[%d]------\n", getpid());
pid_t wpid;
int status;
while(1)
{
wpid=waitpid(-1, &status, WNOHANG);
if(wpid==0)
{
continue;//表示子进程正在运行
}
else if(wpid==-1)
{
printf("no child alive, wpid=[%d]\n", wpid);
exit(0);
}
else if(wpid>0)
{
printf("pid=[%d],status=[%d]\n", wpid, WEXITSTATUS(status));
}
}
}
if(i==0)
{
//第一个子进程
printf("*****pid=[%d]*****\n", getpid());
sleep(5);
return 0;
}
if(i==1)
{
//第二个子进程
printf("*****pid=[%d]*****\n", getpid());
sleep(10);
return 1;
}
if(i==2)
{
//第三个子进程
printf("*****pid=[%d]*****\n", getpid());
sleep(2);
return 2;
}
return 0;
}
parent: pid=[12244] --->打印父进程PID
parent: pid=[12244] --->打印父进程PID
parent: pid=[12244] --->打印父进程PID
------pid=[12244]------ --->i==3的时候是父进程在运行
child: pid=[12245] --->创建的第一个子进程
*****pid=[12245]***** --->i==0的子进程在运行
child: pid=[12247] --->创建的第三个子进程
*****pid=[12247]***** --->i==2的子进程在运行
child: pid=[12246] --->创建的第二个子进程
*****pid=[12246]***** --->i==1的子进程在运行
pid=[12247],status=[2] --->i==2的子进程退出,因为睡眠2S
pid=[12245],status=[0] --->i==0的子进程退出,因为睡眠5S
pid=[12246],status=[1] --->i==1的子进程退出,因为睡眠10S
no child alive, wpid=[-1] --->子进程都退出了,所以返回-1
守护进程daemon
守护进程是linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,一般采用以d结尾的名字。如linux系统服务进程(ftp,nfs等)
特点:
- linux后台服务进程
- 独立于控制终端
- 周期性的执行某种任务(while(1))
- 不能直接和用户交互,不受用户登录、注销的影响
- 一直运行着
进程组
进程组是一个或多个进程的集合。引入进程组是为了简化对进程的管理。当父进程创建子进程时,默认子进程与父进程同属一个进程组
进程组ID等于第一个进程ID(组长进程)
kill -SIGKILL -进程组ID(负数)
来将整个进程组内的进程全部杀死
只要进程组有一个进程存在,那么进程组就存在,与组长进程是否终止无关
会话
一个会话是一个或多个进程组的集合
创建会话的进程不能是进程组组长
ps -ajx
查看进程组ID和会话ID
创建会话的步骤:
父进程先fork子进程,然后父进程退出,让子进程调用setsid函数创建一个会话,这个子进程既是会长也是组长,只要是创建了会话,这个进程就脱离了控制终端的影响。
创建守护进程的模型
1、fork子进程,父进程退出
- 保证子进程不是进程组组长ID
2、子进程调用setsid创建会话
- 子进程成为了新会话的会长
- 并且成了进程组的组长
- 不再受控制终端的影响
3、改变当前工作目录chdir(可选,将启动目录放在不会变化的地方)
4、重设文件掩码mode & ~umask
(可选)
umask(0000)
- 子进程会继承父进程的掩码,避免受限于父进程,增加子进程程序的操作灵活性
5、关闭文件描述符(可选)
- 守护进程不受控制终端的影响,所以可以关闭,释放资源
6、执行守护进程核心代码
例:创建守护进程,每两秒获取一次系统时间,并写到文件里面
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
#include<sys/time.h>
#include<time.h>
#include<fcntl.h>
#include<sys/stat.h>
void func(int signum)
{
//打开文件
int fd = open("mydame.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);
return;
}
int main()
{
//1、父进程fork子进程,父进程退出
pid_t pid = fork();
if(pid < 0 || pid > 0)
{
return 1;
}
//2、子进程调用setsid函数创建会话
setsid();
//3、改变当前工作目录
chdir("/home/duan/桌面");
//4、改变文件掩码
umask(0000);//八进制000
//5、关闭文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
//6、守护进程代码
//注册信号处理函数
struct sigaction act;
act.sa_handler = func;
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);
}
}
优化:不那么频繁的打开关闭文件,并且在文件超过一定大小后,重命名文件进行备份
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
#include<sys/time.h>
#include<time.h>
#include<fcntl.h>
#include<sys/stat.h>
int fd;
int flag=0;
void func(int signum)
{
//获取当前系统时间
time_t t;
time(&t);
//将时间转换成字符数据类型
char *p = ctime(&t);
if(flag==0)
{
//打开文件
int fd = open("mydame.log", O_RDWR | O_CREAT | O_APPEND, 0777);
if(fd<0)
{
return;
}
flag=1;
}
//将时间写入文件
write(fd, p, strlen(p));
return;
}
int main()
{
//1、父进程fork子进程,父进程退出
pid_t pid = fork();
if(pid < 0 || pid > 0)
{
return 1;
}
//2、子进程调用setsid函数创建会话
setsid();
//3、改变当前工作目录
chdir("/home/duan/桌面");
//4、改变文件掩码
umask(0000);
//5、关闭文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
//6、守护进程代码
//注册信号处理函数
struct sigaction act;
act.sa_handler = func;
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)
{
int size = lseek(fd, 0, SEEK_END);
if(size > 100)
{
close(fd);
rename("./mydame.log", "./mydame.log.bak");
flag=0;
}
}
close(fd);
return 0;
}