一、进程与程序
实际上是一堆指令和数据的集合,这个集合反映了一个静态可执行文件和相关的配置文件等。
操作系统可以运行多个程序,那他是如何运行的?实际上,CPU的执行是很快的,而待运行的程序很多,那么为了让操作系统运行多个程序,CPU会把它的执行时间划分成很多段,比如每一段是0.1秒,那么就可以这样A程序运行0.1秒,然后B程序运行0.1,然后C程序运行0.2秒,因为这个切换很快,所以我们感觉程序是同时运行的。
从操作系统上看上面提到的运行程序就是指一个进程,因为存在切换,所以进程管理了很多资源(如打开的文件、挂起的信号、进程状态、内存地址空间等等),也就是说进程参与了CPU的调度,和管理了所有资源,哦,这句话,不是很正确,实际上现代CPU的执行非常非常快,而且操作系统有多个CPU,使用一个进程参与调度时,频繁地从CPU的寄存器和进程堆栈的保存运行状态和对应的信息都很耗时,所以现代CPU将进程仅仅作为一个资源管理的东东,而引入了线程作为CPU调度的基本单位,多个线程可以共享同一进程的所有资源(后面会讲线程)。
注意,程序并不是进程,实际上两个或多个进程不仅有可能执行同一程序,而且还有可能共享地址空间等资源。Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息。它定义在include/linux/sched.h文件中。
注意:子进程复制父进程所拥有的所有资源。详见(二、线程的创建–>提示)
二、线程的创建
头文件:#include <unistd.h>
创建线程的函数:pid_t fork(void);
示例:
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
pid_t id = 0;
int count = 0;
id = fork();
if(id < 0) {
perror("Create Process Failed!\n");
}else if(id == 0) {
// getpid(): 返回调用进程的进程ID(PID)。
printf("I am a child process, my process ID is: %d\n", getpid());
printf("I'm Children!\n");
count += 2;
} else {
printf("I am a parent process, my process ID is: %d\n", getpid());
printf("I'm Children!\n");
count++;
}
printf("统计结果是:%d\n", count);
return 0;
}
提示:
- 调用fork函数后,会创建一个子进程,并且父子两个进程都从fork函数处执行。fork函数对父子进程有不同的返回值,对于父进程来说其返回值是子进程的pid(此时pid > 0),对于子进程来说pid是0。
- 父子进程管理资源的关系如下:
传统的Linux操作系统以统一的方式对待所有进程:子进程复制父进程所拥有的所有资源,但是这种方式使得进程创建非常缓慢,因为子进程需要拷贝父进程所有的地址空间
现在的Linux操作系统主要使用以下3种方式创建进程:
(1) 写时复制
(2) 轻量级进程允许父子进程共享每进程在内核的很多数据结构;例如地址空间、打开文件表和信号处理。
(3) vfork系统调用创建的进程能共享其父进程的内存地址空间,为了防止父进程重写子进程需要的数据,阻塞父进程的执行,一直到子进程退出为止。
三、进程的销毁
头文件:#include <stdlib.h>
销毁线程的函数:void exit(int status);
函数描述:Exit()函数导致正常进程终止,并将status&0377的值返回给父进程。
示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main(int argc, char *argv[]) {
pid_t id = 0;
int count = 0;
int status = 0;
id = fork();
if(id < 0) {
perror("Create Process Failed!\n");
}else if(id == 0) {
// getpid(): 返回调用进程的进程ID(PID)。
printf("I am a child process, my process ID is: %d\n", getpid());
printf("I'm Children!\n");
count += 2;
exit(10);
} else {
printf("I am a parent process, my process ID is: %d\n", getpid());
printf("I'm Parent!\n");
count++;
}
printf("统计结果是:%d\n", count);
// 父进程捕获子进程的状态
wait(&status);
printf("child status: %d\n", WEXITSTATUS(status));
return 0;
}
四、多进程高并发设计
示例程序:
//#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/wait.h>
#include <sys/types.h>
typedef void (*spawn_proc_pt)(void* data);
/********************************************************
* 函数:worker_process_init
* 功能:将进程和CPU核绑定在一起
* 参数:
* worker - 进程号
* 返回:无
*********************************************************/
static void worker_process_init(int worker);
/********************************************************
* 函数:worker_process_cycle
* 功能:
* 参数:
* 返回:无
*********************************************************/
static void worker_process_cycle(void* data);
// 开始创建工作进程
static void start_worker_processes(int n);
pid_t spawn_process(spawn_proc_pt proc, void* data, const char* name);
int main(int argc, char* argv[]) {
start_worker_processes(2);
// 此处写子进程管理代码
// ...
wait(NULL);
return 0;
}
void worker_process_init(int worker) {
cpu_set_t cpu_affinity;
CPU_ZERO(&cpu_affinity);
CPU_SET(worker % CPU_SETSIZE, &cpu_affinity);
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(1);
printf("pid %ld, doing...\n", (long int)getpid());
}
}
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, const char *name) {
pid_t id = fork();
switch(id) {
case -1: // fork 出错
fprintf(stderr, "fork() - failed while spawning \"%s\"", name);
return -1;
case 0: // 子进程
proc(data);
return 0;
default:
break;
}
printf("start %s %ld\n", name, (long int)id);
return id;
}
查看进程在哪个CPU核上运行的命令行命令:ps -o pid,psr,comm -p 进程号
示例:查看进程号2021在哪个CPU核上运行:ps -o pid,psr,comm -p 2021
五、孤儿进程
父进程退出,其一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程收养,并由init进程对他们完成状态收集工作。
模拟孤儿进程的方法:kill 父进程
六、僵尸进程
某个进程使用fork函数创建进程,如果子进程退出,而父进程没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程就称之为僵尸进程
- 僵尸进程的产生
一个进程在调用exit命令结束自己的生命的时候,其实他并没有真正的被销毁,而是留下一个成为僵尸进程(Zombie)的数据结构(系统调用exit,它的作用是 使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)
在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了, 那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是 为什么系统中有时会有很多的僵尸进程。 - 僵尸进程的查看
在shell下使用ps命令,可以查看所有进程的信息。
· 实例程序:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
int pid = 0;
pid = fork();
if(pid < 0) {
fprintf(stderr, "fork - failed!\n");
return -1;
} else if(pid == 0) {
printf("我是子进程:%d!\n", getpid());
exit(27);
} else {
while(1){sleep(1);}
}
return 0;
}
输入:ps -ef
输出:root 3057 3056 0 20:05 pts/0 00:00:00 [processZombie.e] < defunct >
3. 僵尸进程的清除
改写父进程,在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用 wait,内核也会向它发送SIGCHLD消息,尽管对默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。
把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程。它产生的所有僵尸进程也跟着消失。
· 实例程序:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
void clearZombie(int signum) {
//printf("信号值为:%d\n", signum);
int status = 0;
int pid = waitpid(-1, &status, WNOHANG);
printf("子进程[%d]已退出,退出状态为:%d\n", pid, WEXITSTATUS(status));
}
int main(int argc, char **argv) {
signal(SIGCHLD, clearZombie);
int pid = fork();
if(pid < 0) {
fprintf(stderr, "fork - failed!\n");
} else if(pid == 0) {
//sleep(2);
exit(66);
} else {
getchar();
}
return 0;
}
七、守护进程
不与任何终端关联的进程,通常情况下守护进程在系统启动时就在运行,它们以root用户或者其他特殊用户(apache和postfix)运行,并能处理一些系统级的任务。守护进程脱离于终端,是为了避免进程在执行过程中的信息在任何终端上显示,并且进程也不会被任何终端所产生的终端信息所打断(比如关闭终端等)。
那如何成为一个守护进程呢? 步骤如下:
- 调用fork(),创建新进程,它会是将来的守护进程.
- 在父进程中调用exit,保证子进程不是进程组长
- 调用setsid()创建新的会话区
- 将当前目录改成根目录(如果把当前目录作为守护进程的目录,当前目录不能被卸载他作为守护进程的工作目录)
- 将标准输入,标准输出,标准错误重定向到/dev/null.
实例代码:
#include <fcntl.h>
#include <unistd.h>
/***************************************************
* 函数:daemon
* 功能:实现守护进程
* 参数:
* nochdir - 不改变目录(如果不改变传1,否则传0。一般传0)
* noclose - 不重定向标准输入输出(如果不重定向传1,否则传0。一般传0)
* 返回:
* 成功返回0,失败返回-1
****************************************************/
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) {
// chdir(const char *path) - 将调用进程的当前工作目录更改为Path中指定的目录。
(void)chdir("/");
}
if(!noclose && (fd = open("dev/null", O_RDWR, 0)) != -1) {
// dup2(int oldfd, int newfd) - 使用newfd创建一个oldfd的副本。如果newfd之前是打开的,则在重用它之前以静默方式关闭
(void)dup2(fd, STDIN_FILENO);
(void)dup2(fd, STDOUT_FILENO);
(void)dup2(fd, STDERR_FILENO);
if(fd > 2) {
(void)close(fd);
}
}
while(1) { sleep(1); }
return 0;
}
int main(int argc, char **argv) {
daemon(0, 0);
return 0;
}
八、进程间通信
一、进程间通信方式
- 信号
参考如下文章:Linux - 进程间通信方式之信号 - 管道
参考如下文章:Linux - 进程间通信方式之管道 - 使用popen和pclose
- popen的作用:
- 程序A读取程序B的输出(使用fread函数)
- 程序A发送数据给程序B,作为程序B的标准输入(使用fwrite写入)
- popen的原理:
- 使用fork()创建一个子进程
- 在子进程中使用exec执行指定外部程序,并返回一个文件指针FILE*给父进程
- 当使用"r"时,该FILE指针指向外部程序的标准输出
- 当使用"w"时,该FILE指针指向外部程序的标准输入
- popen的优缺点:
- 优点:可以使用shell扩展(比如命令中可以使用通配符),使用方便
- 缺点:每调用一次popen,将要启动两个进程(shell和被指定程序),资源消耗大
- 附popen函数介绍
/****************************************************************************** * 函数:FILE *popen(const char *command, const char *type); * 功能:通过创建管道、派生和调用shell来打开进程。 * 参数: command - 在shell下执行的命令(例:ls -l) type - r(读取)、w(写入)、e(设置close-on-exec标志) * 返回: 如果成功,则返回一个指向可用于读取或写入管道的开放流的指针; 如果fork(2)或管道(2)调用失败,或者函数无法分配内存,则返回NULL。 *******************************************************************************/
- 附pclose函数介绍
/****************************************************************************** * 函数:int pclose(FILE *stream); * 功能:函数等待相关进程终止,并返回wait4(2)返回的命令的退出状态。 * 参数: stream - popen创建的流指针 * 返回: 如果成功,则返回命令的退出状态; 如果wait4(2)返回错误,或者检测到其他错误,则返回-1 *******************************************************************************/
注意:
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE
示例代码 - popen的使用:
/***************************************************************************
* 使用popen执行shell下的ls -l 命令,并将结果输出到控制台
****************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
FILE *stream = popen("ls -l", "r");
if(!stream) {
fprintf(stderr, "popen() - failed!\n");
exit(1);
}
char buffer[2048] = { 0 };
int ret = fread(buffer, sizeof(char), sizeof(buffer), stream);
if(ret > 0) {
buffer[ret] = '\0';
printf("%s\n", buffer);
}
pclose(stream);
return 0;
}
示例代码 - 把输出写入到外部程序:
代码1 - popen主程序:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
FILE *file = popen("./proc_recv.exe", "w");
if(!file) {
fprintf(stderr, "popen() - failed!\n");
exit(1);
}
char buffer[1024] = { 0 };
strcpy(buffer, "popen()示例:将结果输出到外部程序!");
int ret = fwrite(buffer, sizeof(char), sizeof(buffer), file);
pclose(file);
return 0;
}
代码2 - 接受popen输出的数据:
#include <stdio.h>
int main(int argc, char *argv[]) {
char buffer[1024] = { 0 };
scanf("%s", buffer);
printf("receive data: %s\n", buffer);
return 0;
}
二、 消息队列
- 什么是消息队列
消息队列,用于一个进程向另一个进程发送数据。但仅把数据发送到一个"队列"中,而不指定由哪个进程来接收。消息队列,独立于发送消息的进程和接收消息的进程(信号、管道、命名管道都不独立于发送和接收的进程)。
消息队列的最大长度限制:MSGMNB
消息队列中单条消息的最大长度限制:MSGMAX - 消息队列的获取:使用函数msgget
/**************************************************************************
* 原型:int msgget(key_t key, int msgflg);
* 功能:获取与某个键关联的消息队列标识。
* 参数:(与共享内存相似)
key - 消息队列对象的关键字
msgflag - 消息队列的存取权限和建立标志
可取如下值:
IPC_CREAT(如果消息队列不存在,则创建,否则进行打开操作)、
IPC_EXCL(和IPC_CREAT一起使用(使用 | 连接),如果消息对象不存在则创建,否则报错)
* 返回:
* 成功 - 返回消息队列标识符(非负整数)
* 失败 - 返回-1同时errno表示错误
***************************************************************************/
- 消息的发送:使用函数msgsnd
/**************************************************************************
* 原型:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
* 功能:向system V消息队列发送消息。
* 参数:
msqid - 消息队列标识符
msgp - 由用户定义的消息指针结构体(第一个数据成员必须是long类型,用于存放消息类型)
msgsz - 消息大小(减去结构体第一个数据成员的大小)
msgflg - 控制函数行为的标准
取值如下:
0 - 表示忽略
IPC_NOWAIT - 如果包含此项,则消息队列满时,不发送该消息,直接返回-1。如果不包含此项,则消息队列满时,挂起本进程,直到消息队列有空间可用。
* 返回:
* 成功 - 0
* 失败 - 返回-1,并设置errno
***************************************************************************/
- 消息的接收:使用函数msgrcv
/**************************************************************************
* 原型:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
* 功能:从system V消息队列接收消息。
* 参数:
msqid - 消息队列标识符
msgp - 由用户定义的消息指针结构体(第一个数据成员必须是long类型,用于存放消息类型)
msgsz - 消息大小(减去结构体第一个数据成员的大小)
msgtpy - 要接受的消息类型
0 - 从消息队列获取第一个消息,以实现顺序接收(先发先收)
>0 - 从消息队列中获取相同类型的第一个消息
<0 - 从消息队列中获取消息类型小于等于(msgtyp的绝对值)的第一个消息
msgflg - 控制函数行为的标准
取值如下:
0 - 表示忽略
IPC_NOWAIT - 如果包含此项,则消息队列满时,不发送该消息,直接返回-1。如果不包含此项,则消息队列满时,挂起本进程,直到消息队列有空间可用。
* 返回:
* 成功 - 接收到的消息长度(不包含第一个成员)
* 失败 - 返回-1,并设置errno
***************************************************************************/
- 消息的控制:使用函数msgctl
/**************************************************************************
* 原型:int msgctl(int msqid, int cmd, struct msqid_ds *buf);
* 功能:在标识符为msqid的system V消息队列上执行cmd指定的控制操作。
* 参数:
msqid - 消息队列标识符
cmd - 常用命令:
IPC_RMID - 删除消息队列
* 返回:
* 成功 - 返回0
* 失败 - 返回-1,并设置errno
*
* 附 - struct msqid_ds数据结构:
* struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd(2) */
time_t msg_rtime; /* Time of last msgrcv(2) */
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Current number of bytes in queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages in queue */
msglen_t msg_qbytes; /* Maximum number of bytes allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
***************************************************************************/
- 附 - 自定义消息类型示例:
struct my_msg_t {
long int msg_type; // 消息的类型,取>0, 接收消息时可使用该值
// ...
}
示例代码 - 发送数据:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#define MSG_SIZE 1024 // 消息长度
struct my_msg_st {
long int msg_type; // 消息类型
char msg[MSG_SIZE]; // 消息
};
int main(int argc, char *argv[]) {
struct my_msg_st msg;
int msgid = msgget((key_t)1314, 0666 | IPC_CREAT);
if(msgid == -1) {
fprintf(stderr, "msgget() - failed! reason: %s\n", strerror(errno));
exit(1);
}
msg.msg_type = 1;
strcpy(msg.msg, "[message queuing] the sender sends data to the receiver.");
int ret = msgsnd(msgid, &msg, MSG_SIZE, 0);
if(ret == -1) {
fprintf(stderr, "msgsnd() - failed!\n");
exit(2);
}
return 0;
}
示例代码 - 接收数据:
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#define MSG_SIZE 1024
struct my_msg_st {
long int msg_type;
char msg[MSG_SIZE];
};
int main(int argc, char *argv[]) {
int msgid = msgget((key_t)1314, 0666 | IPC_CREAT);
struct my_msg_st msg;
if(msgid == -1) {
fprintf(stderr, "msgget() - failed!\n");
exit(1);
}
msg.msg_type = 0;
int ret = msgrcv(msgid, &msg, MSG_SIZE, 0, 0);
if(ret == -1) {
fprintf(stderr, "msgrcv() - failed!\n");
exit(2);
}
printf("receive data: %s\n", msg.msg);
return 0;
}
示例1:
程序A:循环等待用户输入字符串,每收到一个字符串,就把它发送给程序B,直到用户输入exit。
程序B, 接受程序A发过来的信息,并打印输出。直到接受到exit。
示例代码 - 程序A:
msg.msg_type = 1;
if( msgsnd(msgid, &msg, MSG_SIZE, 0) == -1){
fprintf(stderr, "msgsnd() - failed!\n");
exit(2);
}
if(strncpy(msg.msg, "exit", 4) == 0){
break;
}
}
return 0;
}
示例代码 - 程序B:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#define MSG_SIZE 1024
struct my_msg_st {
long int msg_type;
char msg[MSG_SIZE];
};
int main(int argc, char *argv[]) {
int msgid = msgget((key_t)999, 0666 | IPC_CREAT);
if(msgid == -1) {
fprintf(stderr, "msgget() - failed!\n");
exit(1);
}
int times = 0;
struct my_msg_st msg;
while(1) {
msg.msg_type = 0;
if(msgrcv(msgid, &msg, MSG_SIZE, 0, 0) == -1) {
fprintf(stderr, "msgrvc() - failed!\n");
exit(1);
}
printf("[%d]: %s\n", ++times, msg.msg);
}
return 0;
}
三、信号量
参考文档:Linux - 进程间通信之信号量
四、共享内存
参考文件:进程间通信之共享内存