进程/进程分类/进程间通信(1)--信号

进程

创建进程

创建进程,直接调用fork函数:

#include <unistd.h>

	pid_t fork(void);

负值:创建子进程失败。
0:返回到新创建的子进程。
正值:返回给父亲或调用者。该值包含新创建子进程的进程ID。

#include <unistd.h>
#include <stdio.h>
int main()
{
    pid_t fpid;//fpid表示fork函数返回的值
    int count=0;
    fpid=fork();
    if (fpid < 0)
        printf("error in fork!");
    else if (fpid == 0) {
        printf("i am the child process, my process id is %d\n",getpid());
        printf("I’m children\n");
        count +=2;
    }
    else {
        printf("i am the parent process, my process id is %d\n",getpid());
        printf("I’m parent.\n");
        count++;
    }
    printf("统计结果是: %d\n",count);
    return 0;
}

调用fork函数后,会创建一个子进程,并且父子两个进程都从fork处执行fork函数有两个返回值,对于父进程会返回子进程的pid,此时pid会大于0,对于子进程来说,pid会等于0。

进程是内核调度资源的基本单位,那父子进程管理的资源有什么关系呢?

传统的linux操作系统以统一的方式对待所有的进程:子进程复制父进程所拥有的所有资源,这种方法使得创建进程非常非常非常慢,因为子进程需要拷贝父进程的所有的地址空间,那现代的操作系统,是如何处理的呢?
主要有以下三种方式:

  • 写时复制(如果不更改,就不会复制)
  • 轻量级进程允许父子进程共享每进程在内核的很多数据结构,比如地址空间、打开文件表和信号处理。
  • vfork系统调用创建的进程能共享其父进程的内存地址空间,为了防止父进程重写子进程需要的数据,阻塞父进程的执行,一直到子进程退出为止

销毁进程

exit - 终止正在执行的进程

#include <stdlib.h>

   void exit(int status);
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    pid_t fpid;//fpid表示fork函数返回的值
    int count=0;
    int status = 0;

    fpid=fork();
    if (fpid < 0)
        printf("error in fork!\n");
    else if (fpid == 0) {
        printf("i am the child process, my process id is %d\n",getpid());
        printf("I’m children\n");
        count +=2;
        //exit(-10);  //无符号整数,打印246
        exit(2);
    }
    else {
        printf("i am the parent process, my process id is %d\n",getpid());
        printf("I’m parent.\n");
        count++;
    }
    printf("统计结果是: %d\n",count);
    //父进程捕捉子进程的状态
    wait(&status);
    printf("parent: status: %d\n", WEXITSTATUS(status)); //2
    return 0;
}

wait补充

man wait

在这里插入图片描述

多进程高并发设计

在这里插入图片描述
示例

#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)
{
    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");
    }
}

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;
}

//绑定到cpu的核
static 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_SET(worker % CPU_SETSIZE,&cpu_affinity);// 0 1 2 3 
    //sched_setaffinity
    if(sched_setaffinity(0,sizeof(cpu_set_t),&cpu_affinity) == -1)
    {
       fprintf(stderr,"sched_setaffinity() failed\n");
    }
}

void worker_process_cycle(void *data)

{
     int worker = (intptr_t) data;
    //初始化
     worker_process_init(worker);

    //干活
    for(;;)
    {
      sleep(10);
      printf("pid %ld ,doing ...\n",(long int)getpid());
    }
}


查看进程在cpu的核上执行的命令: ps -eLo ruser,pid,lwp,psr,args

孤儿僵尸守护进程

孤儿进程:

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程所收养,并由init进程对它们完成状态收集工作。
想想我们如何模仿一个孤儿进程? 答案是: kill 父进程!

僵尸进程:

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

  1. 僵尸进程怎样产生的:
       一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用 exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。
      在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装 SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了, 那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是 为什么系统中有时会有很多的僵尸进程。
    怎么查看僵尸进程:
      利用命令ps,可以看到有标记为的进程就是僵尸进程。
    怎样来清除僵尸进程:
      改写父进程,在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用 wait,内核也会向它发送SIGCHLD消息,尽管对默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。
      
      把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程。它产生的所有僵尸进程也跟着消失。

守护进程

不与任何终端关联的进程,通常情况下守护进程在系统启动时就在运行,它们以root用户或者其他特殊用户(apache和postfix)运行,并能处理一些系统级的任务。守护进程脱离于终端,是为了避免进程在执行过程中的信息在任何终端上显示,并且进程也不会被任何终端所产生的终端信息所打断(比如关闭终端等)。那如何成为一个守护进程呢? 步骤如下:

1.调用fork(),创建新进程,它会是将来的守护进程.
2.在父进程中调用exit,保证子进程不是进程组长
3.调用setsid()创建新的会话区
4.将当前目录改成根目录(如果把当前目录作为守护进程的目录,当前目录不能被卸载他作为守护进程的工作目录)
5.将标准输入,标准输出,标准错误重定向到/dev/null.
我们来看这个代码:

#include <fcntl.h>
#include <unistd.h>

int daemon(int nochdir, int noclose)
{
    int fd;

    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);
}

进程间通信

信号

什么是信号?信号是给程序提供一种可以处理异步事件的方法,它利用软件中断来实现。不能自定义信号,所有信号都是系统预定义的。

信号由谁产生?

1)由shell终端根据当前发生的错误(段错误、非法指令等)Ctrl+c而产生相应的信号

比如:
socket通信或者管道通信,如果读端都已经关闭,执行写操作(或者发送数据),将导致执行写操作的进程收到SIGPIPE信号(表示管道破裂)
该信号的默认行为:终止该进程。
2) 在shell终端,使用kill或killall命令产生信号

示例:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>


void myhandle(int sig) 
{
	printf("Catch a signal : %d\n", sig);
}

int main(void) 
{

	signal(SIGINT, myhandle);
	while (1) {
        sleep(1);
        printf("waiting signal\n");
	}
	return 0;
}

输入ctrl+c,SIGINT会捕捉此信号
在这里插入图片描述

 ./a.out   &
 kill  -HUP  13733     /* 向PID为13733的进程发送SIGHUP */        

3)在程序代码中,调用kill系统调用产生信号

常见信号表

信号名称说明
SIGABORT进程异常终止
SIGALRM超时告警
SIGFPE浮点运算异常
SIGHUP连接挂断
SIGILL非法指令
SIGINT终端中断 (Ctrl+C将产生该信号)
SIGKILL*终止进程
SIGPIPE向没有读进程的管道写数据
SIGQUIT终端退出(Ctrl+\将产生该信号)
SIGSEGV无效内存段访问
SIGTERM终止
SIGUSR1*用户自定义信号1
SIGUSR2*用户自定义信号2

以上信号如果不被捕获,则进程接受到后都会终止!

SIGCHLD子进程已停止或退出
SIGCONT*让暂停的进程继续执行
SIGSTOP*停止执行(即“暂停")
SIGTSTP中断挂起
SIGTTIN后台进程尝试读操作
SIGTTOU后台进程尝试写

信号的处理

① 忽略此信号
② 捕捉信号,指定信号处理函数进行处理
③ 执行系统默认动作,大多数都是终止进程

信号的捕获

信号的捕获是指,指定接受到某种信号后,去执行指定的函数。
注意:SIGKILL和SIGSTOP不能被捕获,即,这两种信号的响应动作不能被改变。

signal()

用法:man 2 signal

#include<signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//signum:信号
//sighandler_t:响应的函数,也可以忽略信号,详细如下图

SIG_IGN: 忽略信号
SIG_DFL: 系统默认的方式
或者指定函数

:signal的返回类型,和它的第二个参数,都是函数指针类型
在这里插入图片描述

ps ax | grep ./a.out      //查询进程号
ps -ef | grep ./a.out

sigaction()

(项目实战强烈推荐使用)
用法
man 2 sigaction
在这里插入图片描述
signum 参数指出要捕获的信号类型
act 参数指定新的信号处理方式
oldact 参数输出先前信号的处理方式(如果不为NULL的话)

//结构struct sigaction 
struct sigaction {
	void (*sa_handler)(int);   /* 信号的响应函数 */
	sigset_t   sa_mask;          /* 屏蔽信号集 */                         
	int sa_flags;                /* 当sa_flags中包含 SA_RESETHAND时,\
	接受到该信号并调用指定的信号处理函数执行之后,把该信号的响应行为重置为默认行为SIG_DFL */

//sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数
//sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置
//sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。 

// SA_RESETHAND  简单说就是只想让信号处理函数调用一次
//SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
//SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号

}

补充:
当sa_mask包含某个信号A时,则在信号处理函数执行期间,如果发生了该信号A,
则阻塞该信号A(即暂时不响应该信号),直到信号处理函数执行结束。
即,信号处理函数执行完之后,再响应该信号A

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myhandle(int sig) 
{
	printf("Catch a signal : %d\n", sig);
}

int main(void) 
{
	struct sigaction act;

	act.sa_handler = myhandle;
	sigemptyset(&act.sa_mask);
	//act.sa_flags = 0;
	act.sa_flags = SA_RESETHAND;//只响应一次信号.

	sigaction(SIGINT, &act, 0);

	while (1) {
        sleep(1);
        printf("waiting signal\n");
	}

	return 0;
}

按一次ctrl+c会响应信号,第二次按就会退出.

信号的发送

信号的发送方式:

在shell终端用快捷键产生信号
使用kill,killall命令。
使用kill函数和alarm函数

1) 使用kill函数
给指定的进程发送指定信号

用法: man 2 kill
在这里插入图片描述
注意:

给指定的进程发送信号需要“权限”:
普通用户的进程只能给该用户的其他进程发送信号
root用户可以给所有用户的进程发送信号

kill失败

失败时返回-1

失败原因:

权限不够
信号不存在
指定的进程不存在

实例
创建一个子进程,子进程每秒中输出字符串“child process work!",父进程等待用户输入,如果用户按下字符A, 则向子进程发信号SIGUSR1, 子进程的输出字符串改为大写; 如果用户按下字符a, 则向子进程发信号SIGUSR2, 子进程的输出字符串改为小写

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

int workflag = 0;

void work_up_handle(int sig) 
{
	workflag = 1;
}

void work_down_handle(int sig) 
{
	workflag = 0;
}



int main(void) 
{
	pid_t pd;
	char c;


	pd = fork();
	if (pd == -1) {
		printf("fork error!\n");
		exit(1);
	} else if (pd == 0) {
		char *msg;
		struct sigaction act; 
		act.sa_flags = 0;
		act.sa_handler = work_up_handle;
		sigemptyset(&act.sa_mask);		
		sigaction(SIGUSR1, &act, 0);
		
		act.sa_handler = work_down_handle;
		sigaction(SIGUSR2, &act, 0);
		
		while (1) {
			if (!workflag) {
				msg = "child process work!";
			} else {
				msg = "CHILD PROCESS WORK!";
			}
			printf("%s\n", msg);
			sleep(1);
		}
		
	} else {
		while(1) {
			c = getchar();
			if (c == 'A') {
				kill(pd, SIGUSR1);
			} else if (c == 'a') {
				kill(pd, SIGUSR2);
			}
		}
	}
	

	return 0;
}

实例:“闹钟”,创建一个子进程,子进程在5秒钟之后给父进程发送一个SIGALR,父进程收到SIGALRM信号之后,“闹铃”(用打印模拟)

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

int wakeflag = 0;

void wake_handle(int sig) 
{
	wakeflag = 1;
}

int main(void) 
{
	pid_t pd;
	char c;


	pd = fork();
	if (pd == -1) {
		printf("fork error!\n");
		exit(1);
	} else if (pd == 0) {
		sleep(5);
		kill(getppid(), SIGALRM);
	} else {
		struct sigaction act; 
		act.sa_handler = wake_handle;
		act.sa_flags = 0;
		sigemptyset(&act.sa_mask);

		sigaction(SIGALRM,  &act, 0);

		pause(); //把该进程挂起,直到收到任意一个信号

		if (wakeflag) {
			printf("Alarm clock work!!!\n");
		}
	}

	return 0;
}   
使用alarm()

作用: 在指定时间之内给该进程本身发送一个SIGALRM信号。
用法: man 2 alarm
注意: 时间的单位是“秒”

实际闹钟时间比指定的时间要大一点。  
如果参数为0,则取消已设置的闹钟。
如果闹钟时间还没有到,再次调用alarm,则闹钟将重新定时
每个进程最多只能使用一个闹钟。

返回值:

失败:返回-1
成功:返回上次闹钟的剩余时间(秒)

示例:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>

int wakeflag = 0;

void wake_handle(int sig) 
{
	wakeflag = 1;
}

int main(void) 
{
	int ret;
	
	struct sigaction act;
	act.sa_flags = 0;
	act.sa_handler = wake_handle;
	sigemptyset(&act.sa_mask);
	sigaction(SIGALRM, &act, 0);
	
	printf("time =%ld\n", time((time_t*)0));

	ret = alarm(5);//五秒后发一个信号,只发一次
	if (ret == -1) {
		printf("alarm error!\n");
		exit(1);
	}

	//挂起当前进程,直到收到任意一个信号
	pause();

	if (wakeflag) {
		printf("wake up, time =%ld\n", time((time_t*)0));
	}

	return 0;
}
使用raise
给本进程自身发送信号。
原型: int  raise (int sig)

发送多个信号

某进程正在执行某个信号对应的操作函数期间(该信号的安装函数),如果此时,该进程又多次收到同一个信号(同一种信号值的信号),

则:如果该信号是不可靠信号(<32),则只能再响应一次。
如果该信号是可靠信号(>32),则能再响应多次(不会遗漏)。但是,都是都必须等该次响应函数执行完之后,才能响应下一次。

某进程正在执行某个信号对应的操作函数期间(该信号的安装函数),如果此时,该进程收到另一个信号(不同信号值的信号),则:
如果该信号被包含在当前信号的signaction的sa_mask(信号屏蔽集)中,则不会立即处理该信号。直到当前的信号处理函数执行完之后,才去执行该信号的处理函数。

否则:
则立即中断当前执行过程(如果处于睡眠,比如sleep, 则立即被唤醒)而去执行这个新的信号响应。新的响应执行完之后,再在返回至原来的信号处理函数继续执行。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myhandle(int sig) 
{
	printf("Catch a signal : %d\n", sig);
	int i;
	for (i=0; i<10; i++) {
		sleep(1);
}
	printf("Catch end.%d\n", sig);
}

int main(void) 
{
	struct sigaction act, act2;

	act.sa_handler = myhandle;
	sigemptyset(&act.sa_mask);
	//sigaddset(&act.sa_mask, SIGUSR1);//设置屏蔽,不会被中断 \
    //如果没设置,会被新来的信号中断掉,然后再接着执行
    
       act.sa_flags = 0;

	sigaction(SIGINT, &act, 0);


	act2.sa_handler = myhandle;
	sigemptyset(&act2.sa_mask);
	act2.sa_flags = 0;
	sigaction(SIGUSR1, &act, 0);

	while (1) {

	}

	return 0;
}

获取未处理的信号

当进程的信号屏蔽字中信号发生时,这些信号不会被该进程响应,
可通过sigpending函数获取这些已经发生了但是没有被处理的信号

sigpending

用法man sigpending
返回值
成功则返回0
失败则返回-1

阻塞式等待信号

pause
阻塞进程,直到发生任一信号后
sigsuspend
用指定的参数设置信号屏蔽字,然后阻塞时等待信号的发生。
即,只等待信号屏蔽字之外的信号
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值