进程
前言
fork创建一个子进程,该子进程是父进程的拷贝,获得父进程的栈、数据段、堆、和文本段。两个进程是独立的
fork一次调用返回两次,根据返回值判断是哪个进程和是否成功。
一、进程的创建 fork()/vfork()
1、获得进程id
得到当前进程id:
pid_t getpid(void);
得到当今进程的子进程id:
pid_t getppid(void);
2、fork()
pid_t fork(void);
返回值:
-1 失败
返回为0的是 子进程
返回子进程id的是 父进程
fork是对父进程的拷贝(用户空间,内核空间共享),两个进程相互独立。
对于哪个进程先执行这是未知的,一般调用sleep、信号量、锁机制…来限定随先执行。
fork.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
// ./main
int main(int argc,char *argv[])
{
pid_t id;
int g_val = 100;
int i;
//getpid() 打印执行这个语句的进程的ID
printf("main=%d\n",getpid());
//父进程返回的是子进程的ID号。子进程返回的是0
id=fork();
if(id<0)//fork error
{
printf("fork error\n");
return 1;
}
else if(id==0)//child
{
g_val=1000;
// 注意父进程与子进程的g_val 变量的地址,竟然是相同的,
//但是 ,因为是虚拟地址相同而已,并不是物理地址相同
//所以 还是两个不同的变量
printf("child ,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
}
else
{
sleep(1);
printf("father,id:%d,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",id,getpid(),getppid(),g_val,&g_val);
}
printf("end ,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
return 0;
}
3、vfork()
fork会对父进程的数据段、堆、栈严格的拷贝,也是一种浪费,尤其是在fork后调用exec()下,引入了vfork(),区别fork所在:
①、无需为子进程复制虚拟内存页和页表,相反子进程共享父进程的内存,直至其成功执行exec()/_exit()退出。
②、在子进程调用exec()或_exit()之前,将暂停执行父进程。
pid_t vfork(void);
注意:一般在子进程调用退出时使用 _exit() ,因为其不带刷新stdio缓冲
二、进程的终止 exit()/_exit()/return
1、正常结束进程 exit()
return 0 == exit(0)
void exit(int status);
status值会传到 wait(&status) 中的参数status,但仅有低8位所用,看到status需要 status>>8
return n 等同 exit(n)
带有刷新stdio流缓冲区,
2、结束进程执行 _exit()
void _exit(int status);
调用 _exit()总会成功终止,(即_exit()从不返回)
3、exit()与_exit()区别
exit():带刷新stdio缓冲,_exit()不带
进程是在用户空间内存中维护stdio缓冲 ,fork后会拷贝用户空间的内存包括stdio,在父子进程下调用exit会刷新各自的stdio缓冲区,会导致多次打印输出,使用_exit()就不会。
一般在子进程使用_exit()会退出,父进程使用exit()
注:write 写入的是内核缓冲区,并不会被拷贝
三、监控子进程 wait()/waitpid()
1、wait()
pid_t wait(int *wstatus);
返回: 子进程的id, 错误返回-1
wstatus:子进程退出的返回值,低8位有效 需要status >> 8 查看状态值
eg:
wait(&status);
printf("返回状态值%d",status >> 8);
wait()会暂时停止目前进程的执行(进入阻塞),直到有信号来到或子进程结束。
如果在调用wait()时子进程已经结束,则wait()会立即返回子进程结束状态值
2、waitpid()
等待特定子进程返回
可执行非阻塞等待
得到子进程因什么信号而退出
pid_t waitpid(pid_t pid, int *wstatus, int options);
pid: 子进程id
wstatus:返回状态值
options:返回信息选项
返回值:
成功 子进程id
失败 -1
pid不同值:
pid < -1 等待进程组识别码为pid绝对值的任何子进程。
pid = -1 等待任何子进程,相当于wait()。
pid = 0 等待进程组识别码与目前进程相同的任何子进程。
pid > 0 等待任何子进程识别码为pid的子进程。
参数option可以为0 或下面的OR 组合:
WNOHANG | 如果没有任何已经结束的子进程则马上返回,不予以等待。 |
WUNTRACED | 如果子进程进入暂停执行情况则马上返回,但结束状态不予以理会。 |
子进程的结束状态返回后存于status,底下有几个宏可判别结束情况:
WIFEXITED(status) | 如果子进程正常结束则为非0值。 |
WEXITSTATUS(status) | 取得子进程exit()返回的结束代码,一般会先用WIFEXITED 来判断是否正常结束才能使用此宏。 |
WIFSIGNALED(status) | 如果子进程是因为信号而结束则此宏值为真 |
WTERMSIG(status) | 取得子进程因信号而中止的信号代码,一般会先用WIFSIGNALED 来判断后才使用此宏。 |
WIFSTOPPED(status) | 如果子进程处于暂停执行情况则此宏值为真。一般只有使用WUNTRACED 时才会有此情况。 |
WSTOPSIG(status) | 取得引发子进程暂停的信号代码,一般会先用WIFSTOPPED 来判断后才使用此宏。 |
eg:
if(WIFEXITED(status)) //子进程正常结束
{
prinft("退出状态的: %d"WEXITSTATUS(status)) //打印状态值
}
四、程序的执行 exec()族
1、exec()族
将新程序加载到某一进程的内存空间,将丢弃旧进程,栈,数据段,堆会被替换
int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
int execle(const char *path, const char *arg, ...
/*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
有注释/* */命令列表或数组的结尾需要加 (char*) NULL
eg:
char *argv[] = {"test",NULL};
execlp("./test", "test",(char*)NULL) ;// 执行当前目录下的test程序
execl("./test", "test",(char*)NULL);
execv("./test",argv);
2、system()
int system(const char *command);
eg:
system("./test");
五、僵尸进程和孤儿进程
1、概念
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。(子死,父未知)
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。(父死,子变孤儿)
2、危害与解决方法
危害:
孤儿进程没有危害,因为会被init进程接管,将其释放
僵尸进程不能被系统kill掉,如果产生大量僵尸进程,会占用进程号,使得不能产生新进程。
解决:
①、将僵尸进程变为孤儿进程(将父进程先杀掉,让其子进程变为孤儿进程被init进程收养)
②、在父进程出获取子进程的状态 wait/waitpid(让父进程收尸)
③、通过信号处理机制,子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程
僵尸进程变为孤儿进程例程:
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include<stdio.h>
int main(void)
{
pid_t pid;
printf("main :%d father:%d\r\n",getpid(),getppid());
if ((pid = fork()) < 0) { // 创建第一个子进程
printf("fork error");
} else if (pid == 0)
{ // 进入第一个子进程
if ((pid = fork()) < 0) // 创建第二个子进程
printf("fork error");
else if (pid > 0) // 进入第二个子进程的子进程
exit(0); // 终止第二个子进程的子进程
else
{
// 第二个子进程在睡眠2S后才执行,这样一般情况下第一个子进程会先终止。
sleep(2);
// 这时,第一个子进程肯定终止了,那么它的父进程就自动变成了init。
printf("second child = %d, parent pid = %d\n", getpid(),getppid());
exit(0);
}
}
// 父进程等待并回收第一个子进程
if (waitpid(pid, NULL, 0) != pid)
printf("waitpid error");
// 父进程执行到这里以后,可以退出,也可以执行其他的任务。
printf("cur program:%d,father:%d \r\n",getpid(),getppid());
// 对于刚才那第二个子进程,它继承了父进程的资源,同时它终止后也会被init进程回收,
// 不会成为僵尸进程。
exit(0);
}
信号处理机制例程:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
static void sig_child(int signo);
int main()
{
pid_t pid;
//创建捕捉子进程退出信号
signal(SIGCHLD,sig_child);
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
else if (pid == 0)
{
printf("I am child process,pid id %d.I am exiting.\n",getpid());
exit(0);
}
printf("I am father process.I will sleep two seconds\n");
//等待子进程先退出
sleep(2);
//输出进程信息
system("ps -o pid,ppid,state,tty,command");
printf("father process is exiting.\n");
return 0;
}
static void sig_child(int signo)
{
pid_t pid;
int stat;
//处理僵尸进程
while ((pid = waitpid(-1, &stat, WNOHANG)) >0)
printf("child %d terminated.\n", pid);
}
六、守护进程与后台进程
会话、进程组、进程关系: 会话包含多个进程组、进程组包含多个进程 一个shell就是一个会话
参考:https://www.cnblogs.com/zengyiwen/p/5755191.html
守护进程(Daemon) 是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
后台进程:运行在后台,可受终端控制
区别:
(a、)守护进程已经完全脱离终端控制台了,而后台程序并未完全脱离终端,在终端未关闭前还是会往终端输出结果
(b、)守护进程在关闭终端控制台时不会受影响,而后台程序会随用户退出而停止,需要在以nohup command & 格式运行才能避免影响,比如nohup ./main &
(c、)守护进程的会话组和当前目录,文件描述符都是独立的。后台运行只是终端进行了一次fork,让程序在后台执行,这些都没改变。
守护进程创建:https://blog.youkuaiyun.com/mijichui2153/article/details/81394387
①、自己实现,调用两次fork (setsid、chdir、umask、close)
将fork后将父进程退出、子进程设置会话setsid 后再fork子进程,以及退出父进程,得到守护进程,再设置工作目录 chdir 、 设置文件权限掩码umask、关闭不需要文件描述符。创建信号处理函数来关闭守护进程
②、库函数daemon()
创建一个守护进程,每分钟在/home/book下的daemon.log 打印log 。
查看进程 : ps -axj | grep 进程名 杀: kill -9 pid
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include <stdio.h>
static int flag = 1;
void create_daemon();
void handler(int);
int main()
{
time_t t;
int fd;
create_daemon();
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if(sigaction(SIGQUIT, &act, NULL))
{
printf("sigaction error.\n");
exit(0);
}
while(flag)
{
fd = open("/home/book/daemon.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
if(fd == -1)
{
printf("open error\n");
}
t = time(0);
char *buf = asctime(localtime(&t));
write(fd, buf, strlen(buf));
close(fd);
sleep(60);
}
return 0;
}
void handler(int sig)
{
printf("I got a signal %d\nI'm quitting.\n", sig);
flag = 0;
}
void create_daemon()
{
pid_t pid;
/*(1)-----创建一个进程来用作守护进程-----*/
pid = fork();
if(pid == -1)
{
printf("fork error\n");
exit(1);
}
/*(1.1)-----------原父进程退出-------------*/
else if(pid)
{
exit(0);
}
/*(2)---setsid使子进程独立。摆脱会话控制、摆脱原进程组控制、摆脱终端控制----*/
if(-1 == setsid())
{
printf("setsid error\n");
exit(1);
}
/*(3)---通过再次创建子进程结束当前进程,使进程不再是会话首进程来禁止进程重新打开控制终端----*/
pid = fork();
if(pid == -1)
{
printf("fork error\n");
exit(1);
}
else if(pid)
{
exit(0);
}
/*(4)---子进程中调用chdir()让根目录成为子进程工作目录----*/
chdir("/");
int i;
/*(6)---关闭文件描述符(常说的输入,输出,报错3个文件)----*/
for(i = 0; i < getdtablesize(); ++i)
{
close(i);
}
/*(5)---重设文件掩码为0(将权限全部开放)----*/
umask(0);
return;
}
参考:https://www.cnblogs.com/Anker/p/3271773.html
https://www.cnblogs.com/scut-fm/p/3393686.html