程序和进程
程序≠进程
产生进程
创建进程——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,并设置errno
为EINTR
,表示被信号打断。
使用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);