进程
程序和进程
操作系统可以运行多个程序,那他是如何运行的?实际上,CPU的执行是很快的,而待运行的程序很多,那么为了让操作系统运行多个程序,CPU会把它的执行时间划分成很多段,比如每一段是0.1秒,那么就可以这样A程序运行0.1秒,然后B程序运行0.1,然后C程序运行0.2秒,因为这个切换很快,所以我们感觉程序是同时运行的。
产生进程
- 创建进程很简单,直接调用fork函数:pid_t fork(void);
创建进程用法举例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
int pid = 0;// 父子进程不共享局部变量
// pid_t fork(void);
pid_t fpid = fork(); // fpid表示fork函数返回的值
if(fpid < 0){
printf("创建子进程失败\n");
return fpid;
}else if(fpid == 0){
// 创建成功后子进程中fpid == 0
pid = getpid();
printf("子进程,子进程pid = %d\n",getpid());
}else if(fpid > 0){
// 创建成功后父进程的fpid 为子进程的pid
pid = getpid();
printf("父进程,父进程pid = %d\n",getpid());
printf("父进程,子进程pid = %d\n",getpid());
}
// 再创建3个子进程用于观察
for(int i = 3 ; i > 0 && fpid > 0 ; i--){
fpid = fork();
}
printf("pid = %d\n",pid);
while (1);
return 0;
}
调用fork函数后,会创建一个子进程,并且父子两个进程都从fork处执行,fork函数有两个返回值,对于父进程会返回子进程的pid,此时pid会大于0,对于子进程来说,pid会等于0。
局部变量:写时复制,读时共享
销毁进程
exit - 终止正在执行的进程
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int pid = 0;
// pid_t fork(void);
pid_t fpid = fork();
if (fpid < 0)
{
printf("创建子进程失败\n");
return fpid;
}
else if (fpid == 0)
{
printf("我是子进程,马上就要exit了\n");
exit(-1);
printf("子进程还活着\n");
}
else if (fpid > 0)
{
printf("父进程还活着\n");
}
while (1);
return 0;
}
这里子进程已经退出,但是父进程没有执行完毕,导致子进程没有回收变成僵尸进程
多进程高并发设计
优点:
-
若要用多进程实现高并发那么自己linxu系统cpu有多少核,就产生多少个进程多个核可以并发执行只有多个进程在不同的核上运行才可以充分利用多核系统的并发处理能力
-
比如:cpu为四核,那么主进程就产生4个子进程,让四个子进程分别在不同的核上运行,
-
4个子进程在工作的时候,把任务放在一个共享内存中(通过内存映射,使多个进程可以访问一个内存),哪个任务做完了,就接着拿任务给每个子进程的负载均衡
-
职责明确:父进程管理生死,子进程工作
查看cpu核数
cat /proc/cpuinfo
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
typedef void (*spawn_proc_pt)(void *data);
static void worker_process_cycle(void *data);
static void start_worker_processes(int n);
pid_t spawn_process(spawn_proc_pt proc, void *data, char *name);
int main(int argc, char **argv)
{
// 子进程用于工作,父进程用于管理子进程
// 开启工作进程,创建4个子进程
start_worker_processes(4);
// 管理子进程
wait(NULL);
}
// 开启工作进程
void start_worker_processes(int n)
{
int i = 0;
for (i = n - 1; i >= 0; i--)
{
// 创建子进程
spawn_process(worker_process_cycle, (void *)(intptr_t)i, "worker process");
}
}
// 创建子进程
// spawn_proc_pt 类型的函数:void worker_process_cycle(void *data)
pid_t spawn_process(spawn_proc_pt proc, void *data, char *name)
{
pid_t pid;
pid = fork(); // 创建子进程
switch (pid)
{
case -1:
fprintf(stderr, "fork() failed while spawning \"%s\"\n", name);
return -1;
case 0:
proc(data); // 成功创建子进程后让子进程去工作
return 0;
default:
break;
}
printf("start %s %ld\n", name, (long int)pid);
return pid;
}
// 给进程安排工作
void worker_process_cycle(void *data)
{
// data 其实是一个int*类型
int worker = (intptr_t)data;
// 初始化
worker_process_init(worker);
// 干活
for (;;)
{
sleep(10);
printf("pid %ld ,doing ...\n", (long int)getpid());
}
}
// 初始化进程
void worker_process_init(int worker)
{
cpu_set_t cpu_affinity;
// worker = 2;
// 多核高并发处理 4core 0 - 0 core 1 - 1 2 -2 3 -3
CPU_ZERO(&cpu_affinity); // 结构体清零
// CPU_SETSIZE:1024 支持cpu最大的数量
CPU_SET(worker % CPU_SETSIZE, &cpu_affinity); // 0 1 2 3 设置核
/*
在多CPU系统中,通过sched_setaffinity ()可以设置进程的CPU亲和力,
使进程绑定在某一个或几个CPU上运行,避免在CPU之间来回切换,从而提高该进程的实时性能。
*/
if (sched_setaffinity(0, sizeof(cpu_set_t), &cpu_affinity) == -1)
{
fprintf(stderr, "sched_setaffinity() failed\n");
}
}
查看进程对应的核数
代码中函数指针不太理解的看看下方代码:
// typedef 返回类型(*新类型)(参数表)
typedef char (*PTRFUN)(int);
PTRFUN pFun;
char glFun(int a){ return;}
void main()
{
pFun = glFun;
(*pFun)(2); //调用函数
}
查看进程在cpu的核上执行的命令: ps -eLo ruser,pid,lwp,psr,args
孤儿僵尸守护进程
孤儿进程:
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程所收养,并由init进程对它们完成状态收集工作。
想想我们如何模仿一个孤儿进程? 答案是: kill 父进程!
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
int fpid = 1;// 父子进程不共享局部变量
for(int i = 5 ; i > 0 && fpid > 0 ; i--){
fpid = fork();
}
if(fpid < 0){
printf("创建子进程失败\n");
return fpid;
}else if(fpid == 0){
while(1);
}else if(fpid > 0){
while(1);
}
return 0;
}
守护进程(重点)
把孤儿进程做成守护进程
不与任何终端关联的进程,通常情况下守护进程在系统启动时就在运行,它们以root用户或者其他特殊用户(apache和postfix)运行,并能处理一些系统级的任务。守护进程脱离于终端,是为了避免进程在执行过程中的信息在任何终端上显示,并且进程也不会被任何终端所产生的终端信息所打断(比如关闭终端等)。那如何成为一个守护进程呢? 步骤如下:
- 1.调用fork(),创建新进程,它会是将来的守护进程.
- 2.在父进程中调用exit(父进程挂掉),保证子进程不是进程组长
- 3.调用setsid()创建新的会话区 – 具体先不追究
- 4.将当前目录改成根目录(如果把当前目录作为守护进程的目录,当前目录不能被卸载他作为守护进程的工作目录)
- 5.将标准输入,标准输出,标准错误重定向到/dev/null.
我们来看这个代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
// 一般传入 0 , 0
int daemon(int nochdir, int noclose)
{
int fd = 0;
switch (fork())
{
case -1:
return (-1);
case 0:
break;
default:
_exit(0); // 父进程自杀
}
if (setsid() == -1) // 创建会画
return (-1);
if (!nochdir) // 将当前目录改成根目录
(void)chdir("/");
if (!noclose && (fd = open("/dev/null", O_RDWR, 0)) != -1)
{
// 重定向 标准输入 标准输出 标准出错
(void)dup2(fd, STDIN_FILENO);
(void)dup2(fd, STDOUT_FILENO);
(void)dup2(fd, STDERR_FILENO);
if (fd > 2)
(void)close(fd);
}
return (0);
}
int main()
{
// 创建子进程,并且把父进程杀死
daemon(0,0);
printf("hello\n");
while(1);// 处理事务
return 0;
}
父进程自杀后,守护进程 init 接管 , 一直在后台进行运行
僵尸进程:
一个进程使用fork创建子进程,如果子进程退出(exit),而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
- 僵尸进程怎样产生的:
- 一个进程使用fork创建子进程,如果子进程退出(exit),而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
- 它需要它的父进程来为它收尸,如果他的父进程没安装 SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了, 那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。
1.怎么查看僵尸进程:
利用命令ps,可以看到有标记为的进程就是僵尸进程。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main(){
pid_t fpid = fork();
if(fpid < 0){
printf("创建子进程失败\n");
return fpid;
}else if(fpid == 0){
exit(-1);
}else if(fpid > 0){
while(1);
}
return 0;
}
这里可以用 wait 和 waitpid 函数回收子进程
2.怎样来清除僵尸进程:
- 改写父进程,在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用 wait,内核也会向它发送SIGCHLD消息,尽管对默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。
- 把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程。它产生的所有僵尸进程也跟着消失。