(自用)多进程与信号

发布于:2024-07-10 ⋅ 阅读:(152) ⋅ 点赞:(0)
程序和进程

程序≠进程

产生进程

创建进程——fork函数

函数原型

#include <unistd.h>
pid_t fork(void);

函数功能:

fork函数的功能是创建一个与当前进程几乎完全相同的子进程。这个“几乎完全相同”指的是子进程会复制父进程的代码段、数据段、BSS段、堆、栈等所有用户空间信息,但它们在内核中的进程控制块(PCB)是不同的,因此拥有不同的进程ID。

返回值

fork函数的一个独特之处在于它“调用一次,返回两次”:

  • 在父进程中,fork函数返回新创建的子进程的进程ID。
  • 在子进程中,fork函数返回0。
  • 如果fork函数调用失败,则返回-1,并设置相应的errno值以指示错误原因(如EAGAIN表示达到进程数上限,ENOMEM表示系统内存不足)。

代码示例:

fork_test.c

#include <unistd.h>
#include <stdio.h>
#include<sys/types.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;
}

分析:

最后父子进程都输出count的值

子进程输出2 父进程输出1

因为子进程、父进程的count不是同一份,子进程复制了父进程的count

注意:子进程、父进程getpid()输出的都是自己的进程id(子进程执行getpid()返回的不是0)

操作系统优化

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

  • 写时复制

以fork_test.c为例,只有在父子进程对于count进行修改的时候,才对count进行复制。

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

进程销毁——exit函数

函数原型
 #include <stdlib.h>
 void exit(int status);
函数功能

进程调用exit函数时,进程结束,并返回状态码(保存在status中)

代码示例
 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;
     exit(2);
 }

子进程以状态码2退出

父进程等待子进程结束——wait函数

函数原型
#include<sys/wait.h>
pid_t wait(int* _NULL wstatus)
函数功能

父进程调用wait函数来等待子进程结束

注意:1.wstatus地址所保存的值不是子进程退出的状态码,要用宏函数WEXITSTATUS

2.且wstatus可以为NULL

代码示例
#include <unistd.h>
#include <stdio.h>
#include<sys/types.h>
#include<sys/wait.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!");
    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(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));
    return 0;
}

多进程高并发设计

不同的work进程在不同的核上运行

示例代码

下列代码创建了2个工作进程并绑定在CPU的不同核上工作,它们的工作内容为每隔10s输出“pid %ld ,doing ...\n", (long int)getpid()

fork_work_test.c

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include<sys/wait.h>
#include<errno.h>
#include<string.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(2);
    //管理子进程
    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;
}


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,error desc:%s\n",strerror(errno));
    }
}

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

解析:

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

proc表示要进程执行的函数名、data表示proc所需要的参数、name表示进程名

该函数会创建一个子进程去完成proc任务,父进程打印信息,eg:start work process 5436

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

在本例中,worker_process_cycle为表示分配给每个work_process的任务,它先进行初始化(主要是让每个进程关联对应的核),然后执行输出任务

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,error desc:%s\n",strerror(errno));
    }
}

该函数用于设置进程在CPU的哪个核上工作

cpu_set_t

cpu_set_t 本质上是一个位集合(bit set),每一位代表一个CPU核心。如果一个位的值为1,表示该位对应的CPU核心包含在集合中;如果为0,则表示该CPU核心不在集合中。通过这种方式,cpu_set_t 能够表示一个或多个CPU核心的集合。

相关函数:

  • sched_setaffinity():该函数用于设置指定进程(或线程)的CPU亲和性,即指定该进程(或线程)应该在哪一个或哪些CPU核心上运行。
  • sched_getaffinity():与sched_setaffinity()相对应,这个函数用于获取指定进程(或线程)当前的CPU亲和性设置。
  • CPU_ZERO():用于初始化cpu_set_t变量,将所有位清零,即表示不包含任何CPU核心。
  • CPU_SET():用于将指定的CPU核心添加到cpu_set_t变量表示的集合中。
  • CPU_CLR():与CPU_SET()相反,用于从cpu_set_t变量表示的集合中移除指定的CPU核心。
  • CPU_ISSET():用于检查指定的CPU核心是否包含在cpu_set_t变量表示的集合中。

查看进程在cpu的核上执行

ps -eLo ruser,pid,lwp,psr,args | grep 可执行文件名

上图画圈部分表示进程运行的核的编号

僵尸_孤儿_守护进程(面试可能会问的概念)

  • 孤儿进程:
  • 一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程所收养,并由init进程对它们完成状态收集工作。

想想我们如何模仿一个孤儿进程?  答案是:  kill 父进程! 

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

子进程完成任务,但父进程没有执行wait或者waitpid,则子进程变为僵尸进程

特点

占用进程表项但不占用其他资源,可能会导致进程表溢出,从而影响新进程的创建。

PS:父进程提前结束,那么子进程的父进程会变为init,子进程结束后init进程会给子进程收尸

如果父进程陷入循环且没有调用wait或者waitpid,则子进程结束后,无人为其收尸,它会持续僵尸进程的状态.

查看僵尸进程

利用命令ps,可以看到有标记为<defunct>的进程就是僵尸进程。

守护进程
特点

1.不与任何终端关联

2.以特殊用户运行 (例如root)

3.处理一些系统级任务

成为守护进程的步骤

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("/");//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);
    }//将标准输入,标准输出,标准错误重定向到/dev/null.
    return (0);
}

进程间通信

信号

不能自定义,所有信号都是系统预定义的。

注意:SIGKILL和SIGSTOP不能被捕获,即,这两种信号的响应动作不能被改变

信号的捕获——signal函数、sigaction函数

signal函数
函数原型
#include<signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);

handler可以设为三种方式:

1.SIG_IGN 对signum对应的信号进行忽略
2.SIG_DFL对signum对应的信号进行默认处理

3.函数名,接收到signum对应的信号时,执行对应函数且函数的返回值一定要为void,参数一定要为int

返回值

signal的返回类型和第二个参数的类型都是函数指针。

sigaction函数(项目实战强烈推荐使用)

 sigaction与signal的区别: sigaction比signal更“健壮”,建议使用sigaction

函数原型
#include<signal.h>
int sigaction(int signum,
                     const struct sigaction *_Nullable restrict act,
                     struct sigaction *_Nullable restrict oldact);

关于信号处理的相关配置都放在struct sigaction结构体中,

act表示新的配置,oldact用来储存旧的配置。

sigaction结构体
struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);
           };

(*sa_handler)(int): 信号处理函数

sa_mask:信号屏蔽集,在执行信号处理函数时,暂时阻塞该集合内的信号(等到信号处理函数执行结束后再响应)。

信号集相关的函数:

a.sigemptyset():用于初始化一个空的信号集,即将所有信号设置为未阻塞状态。

用法示例:

#include <signal.h>

sigset_t set;
sigemptyset(&set);

b.sigfillset(sigset_t *set):该函数用于将所有信号添加到信号集set中,即将所有信号设置为阻塞状态

用法示例:

#include <signal.h>

sigset_t set;
sigfillset(&set);

c.sigaddset(sigset_t *set, int signum):该函数用于将指定信号signum添加到信号集set中,将指定信号设置为阻塞状态。

eg:

#include <signal.h>

sigset_t set;
sigaddset(&set, SIGINT); // 将SIGINT信号添加到信号集中

d.sigdelset(sigset_t *set, int signum):该函数用于将指定信号signum从信号集set中删除,将指定信号设置为未阻塞状态。

eg:

#include <signal.h>

sigset_t set;
sigdelset(&set, SIGINT); // 将SIGINT信号从信号集中删除

e.int sigismember(const sigset_t *set, int signum):用于判断指定信号是否属于set信号集。

sigismember 函数返回一个整型值,如果指定的信号 signum 包含在 set 所指向的信号集合中,则返回非零值;否则返回 0

为0表示默认的行为

sa_flags:当sa_flags中包含 SA_RESETHAND时,接受到该信号并调用指定的信号处理函数执行之后,把该信号的响应行为重置为默认行为SIG_DFL

代码示例
#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;

	sigaction(SIGINT, &act, 0);

	while (1) {
        
	}

	return 0;
}

信号的发送

信号的发送方式:

      1.在shell终端用快捷键产生信号

      2.使用kill,killall命令。

      3.使用kill函数和alarm函数

使用kill函数发命令
函数原型
#include <signal.h>
int kill(pid_t pid, int sig);

向进程号为pid的进程发送sig信号

返回值

成功时返回0,

失败时返回-1,并设置errno

代码示例

实例:main6.c创建一个子进程,子进程每秒中输出字符串“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;
}

实例:main7.c “闹钟”,创建一个子进程,子进程在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;
}

上面代码中用到了pause函数:

pause函数

函数原型

#include <unistd.h>

int pause(void);

调用pause()函数会使当前进程挂起,并等待信号到来。当进程接收到一个信号时,pause()函数返回 -1,并设置errnoEINTR,表示被信号打断。

使用alarm函数发送信号——进程自己给自己发送SIGALRM信号
函数原型
#include <unistd.h>

unsigned int alarm(unsigned int seconds);
函数功能

 调用alarm(seconds)函数会设置一个定时器,在经过seconds秒后,向当前进程发送一个SIGALRM信号。如果之前已经设置过定时器,新的定时器将会覆盖之前的定时器。

返回值

如果之前已经设置过定时器,则返回上一个定时器剩余时间未到期的秒数;如果之前没有设置过定时器,则返回0.

注意:时间的单位是“秒”

                 实际闹钟时间比指定的时间要大一点。  

                 如果参数为0,则取消已设置的闹钟。

                 如果闹钟时间还没有到,再次调用alarm,则闹钟将重新定时

                 每个进程最多只能使用一个闹钟。

raise函数——进程给自己发送信号(用的比较少)

函数原型

#include<signal.h>
int raise(int sig);

发送多个信号
重复收到同一信号

则:如果该信号是不可靠信号(<32),则只能再响应一次。

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

执行信号处理函数的时候收到其他信号

    如果该信号被包含在当前信号的signaction的sa_mask(信号屏蔽集)中,则不会立即处理该信号。直到当前的信号处理函数执行完之后,才去执行该信号的处理函数。

    否则:

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

设置进程的信号屏蔽字——sigprocmask函数

函数原型

 #include <signal.h>

       /* Prototype for the glibc wrapper function */
       int sigprocmask(int how, const sigset_t *_Nullable restrict set,
                                  sigset_t *_Nullable restrict oldset);

参数:

             how:

                   SIG_BLOCK      把参数set中的信号添加到信号屏蔽字中

                   SIG_UNBLOCK  把参数set中的信号从信号屏蔽字中删除

                   SIG_SETMASK  把参数set中的信号设置为信号屏蔽字

             

             oldset

                  返回原来的信号屏蔽字

返回值

  • 如果函数执行成功,则返回 0。
  • 如果函数执行失败,则返回 -1,并设置 errno 变量来指示错误的原因

代码示例

main4_3.c

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

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

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

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

	sigset_t proc_sig_msk, old_mask;
	sigemptyset(&proc_sig_msk);
	sigaddset(&proc_sig_msk, SIGINT);

	sigprocmask(SIG_BLOCK, &proc_sig_msk, &old_mask);
	sleep(5);
	printf("had delete SIGINT from process sig mask\n");
	sigprocmask(SIG_UNBLOCK, &proc_sig_msk, &old_mask);
		
	while (1) {
		
	}

	return 0;
}

                  

获取未处理的新型号——sigpending函数

 当进程的信号屏蔽字中信号发生时,这些信号不会被该进程响应,

  可通过sigpending函数获取这些已经发生了但是没有被处理的信号

函数原型

#include <signal.h>

       int sigpending(sigset_t *set);

返回值

成功返回0,失败返回-1

阻塞式等待信号

 (1) pause

            阻塞进程,直到发生任一信号后

       

       (2) sigsuspend

            用指定的参数设置信号屏蔽字,然后阻塞时等待信号的生。

            即,只等待信号屏蔽字之外的信号

 #include <signal.h>

       int sigsuspend(const sigset_t *mask);


网站公告

今日签到

点亮在社区的每一天
去签到