目录
进程
创建进程
创建进程,直接调用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获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
- 僵尸进程怎样产生的:
一个进程在调用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
用指定的参数设置信号屏蔽字,然后阻塞时等待信号的发生。
即,只等待信号屏蔽字之外的信号