目录
一、程序和进程
我们经常谈论程序,实际上就是一堆指令和数据的集合,这个集合反映在了一个静态可执行文件和相关的配置文件等,而进程是什么呢?
操作系统可以运行多个程序,那他是如何运行的?实际上,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文件中。
二、进程的创建
创建进程很简单,直接调用fork函数:
#include <unistd.h>
pid_t fork(void);
创建进程用法举例:
#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);
DESCRIPTION
The exit() function causes normal process termination and the value of status & 0377 is returned to the parent (see wait(2)).
#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);
}
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;
}
四、多进程高并发设计
#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;
}
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,可以看到有标记为<defunct>的进程就是僵尸进程。
怎样来清除僵尸进程:
改写父进程,在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用 wait,内核也会向它发送SIGCHLD消息,尽管对默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。
把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程。它产生的所有僵尸进程也跟着消失。
守护进程
不与任何终端关联的进程,通常情况下守护进程在系统启动时就在运行,它们以root用户或者其他特殊用户(apache和postfix)运行,并能处理一些系统级的任务。守护进程脱离于终端,是为了避免进程在执行过程中的信息在任何终端上显示,并且进程也不会被任何终端所产生的终端信息所打断(比如关闭终端等)。那如何成为一个守护进程呢?
步骤如下:
调用fork(),创建新进程,它会是将来的守护进程.
在父进程中调用exit,保证子进程不是进程组长调用setsid()创建新的会话区
将当前目录改成根目录(如果把当前目录作为守护进程的目录,当前目录不能被卸载他作为守护进程的工作目录)
将标准输入,标准输出,标准错误重定向到/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、信号
什么是信号?信号是给程序提供一种可以处理异步事件的方法,它利用软件中断来实现。不能自定义信号,所有信号都是系统预定义的。
1.1、信号由谁产生
由shell终端根据当前发生的错误(段错误、非法指令等)Ctrl+c而产生相应的信号
比如:
socket通信或者管道通信,如果读端都已经关闭,执行写操作(或者发送数据),
将导致执行写操作的进程收到SIGPIPE信号(表示管道破裂)
该信号的默认行为:终止该进程。
在shell终端,使用kill或killall命令产生信号
1.2、实例
#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);
}
return 0;
}
./a.out &
kill -HUP 13733 /* 向PID为13733的进程发送SIGHUP */
1.3、信号的类型
信号名称 说明
-------------------------------------------
SIGABORT 进程异常终止
SIGALRM 超时告警
SIGFPE 浮点运算异常
SIGHUP 连接挂断
SIGILL 非法指令
SIGINT 终端中断 (Ctrl+C将产生该信号)
SIGKILL *终止进程
SIGPIPE 向没有读进程的管道写数据
SIGQUIT 终端退出(Ctrl+\将产生该信号)
SIGSEGV 无效内存段访问
SIGTERM 终止
SIGUSR1 *用户自定义信号1
SIGUSR2 *用户自定义信号2
-------------------------------------->以上信号如果不被捕获,则进程接受到后都会终止!
SIGCHLD 子进程已停止或退出
SIGCONT *让暂停的进程继续执行
SIGSTOP *停止执行(即“暂停")
SIGTSTP 中断挂起
SIGTTIN 后台进程尝试读操作
SIGTTOU 后台进程尝试写
-------------------------------------------
1.4、信号的处理
忽略此信号
捕捉信号,指定信号处理函数进行处理
执行系统默认动作,大多数都是终止进程
1.5、信号的捕捉
信号的捕获,是指,指定接受到某种信号后,去执行指定的函数。
注意:SIGKILL和SIGSTOP不能被捕获,即,这两种信号的响应动作不能被改变。
1.6、使用signal
用法:man 2 signal
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
注:signal的返回类型,和它的第二个参数,都是函数指针类型
signal的参数2可去以下特殊值:
SIG_IGN 忽略信号
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)
{
signal(SIGINT, myhandle);
while (1) {
sleep(1);
}
return 0;
}
此时就不能结束该进程了!
只能通过其他终端,给该进程发送一个其他信号,使它终止
#ps ax | grep ./a.out //查询进程号
#kill -HUP 进程号
恢复信号的默认行为main3.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myhandle(int sig)
{
static int cnt = 0;
printf("Catch a signal : %d\n", sig);
signal(SIGINT, SIG_DFL); //等同于signal(sig, SIG_DFL);
}
int main(void)
{
signal(SIGINT, myhandle);
while (1) {
sleep(1);
}
return 0;
}
使用SIG_DFL时,仅当第一次调用自定义的行为后马上使用SIG_DFL就可恢复,如果连续捕获多次后,就不确定。
1.7、使用sigaction
sigaction与signal的区别: sigaction比signal更“健壮”,建议使用sigaction
用法:man 2 sigaction
结构struct sigaction
struct sigaction {
void (*sa_handler)(int); /* 信号的响应函数 */
sigset_t sa_mask; /* 屏蔽信号集 */
int sa_flags; /* 当sa_flags中包含 SA_RESETHAND时,接受到该信号并
调用指定的信号处理函数执行之后,把该信号的响应行为重置为默认行为SIG_DFL */
...
}
补充:
当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;
sigaction(SIGINT, &act, 0);
while (1) {
}
return 0;
}
用sigaction改变响应动作
#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) {
}
return 0;
}
用sigaction恢复默认动作
1.8、信号的发送
信号的发送方式:
在shell终端用快捷键产生信号
使用kill,killall命令。
使用kill函数和alarm函数
1) 使用kill函数
给指定的进程发送指定信号
用法:man 2 kill
注意:
给指定的进程发送信号需要“权限”:
普通用户的进程只能给该用户的其他进程发送信号
root用户可以给所有用户的进程发送信号
kill失败
失败时返回-1
失败原因:
权限不够
信号不存在
指定的进程不存在
实例: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;
}
2) 使用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;
}
3) 使用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;
}
信号集
什么是信号集
信号集,用sigset_t类型表示,实质是一个无符号长整形。
用来表示包含多个信号的集合。
信号集的基本操作
sigemptyset 把信号集清空
sigfillset 把所有已定义的信号填充到指定信号集
sigdelset 从指定的信号集中删除指定的信号
sigaddset 从指定的信号集中添加指定的信号
sigismember 判断指定的信号是否在指定的信号集中
如果是, 返回 1
如果不是, 返回 0
信号无效, 返回-1
进程的“信号屏蔽字”
进程的“信号屏蔽字”是一个信号集
想目标进程发送某信号时,如果这个信号在目标进程的信号屏蔽字中,
则目标进程将不会捕获到该信号,即不会执行该信号的处理函数。
当该进程的信号屏蔽字不再包含该信号时,则会捕获这个早已收到的信号(执行对应的函数)
修改进程的“信号屏蔽字”
使用sigprocmask
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
how:
SIG_BLOCK 把参数set中的信号添加到信号屏蔽字中
SIG_UNBLOCK 把参数set中的信号从信号屏蔽字中删除
SIG_SETMASK 把参数set中的信号设置为信号屏蔽字
oldset
返回原来的信号屏蔽字
实例:
#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函数获取这些已经发生了但是没有被处理的信号
用法: man sigpending
返回值:成功则返回0
失败则返回-1
阻塞式等待信号
pause
阻塞进程,直到发生任一信号后
sigsuspend
用指定的参数设置信号屏蔽字,然后阻塞时等待信号的发生。
即,只等待信号屏蔽字之外的信号
2、管道
IPC 有多种方式, 管道是IPC的最基本的方式.
管道是“半双工”的,即是单向的。
管道是FIFO(先进先出)的。
单进程中的管道:
int fd[2]
使用文件描述符fd[1], 向管道写数据
使用文件描述符fd[0], 从管道读数据
注:单进程中的管道无实际用处
管道用于多进程间通信。
2.1、管道的创建
使用pipe系统调用
用法:见 man 2 pipe
返回值:
成功:返回 0
失败:返回 -1
注意:获取两个“文件描述符”
分别对应管道的读端和写端。
fd[0]: 是管道的读端
fd[1]: 是管道的写端
如果对fd[0]进行写操作,对fd[1]进行读操作,可能导致不可预期的错误。
2.2、管道的使用
实例1:单进程使用管道进行通信
注意:创建管道后,获得该管道的两个文件描述符,
不需要普通文件操作中的open操作
如图:
实例:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
int fd[2];
int ret;
char buff1[1024];
char buff2[1024];
ret = pipe(fd);
if (ret !=0) {
printf("create pipe failed!\n");
exit(1);
}
strcpy(buff1, "Hello!");
write(fd[1], buff1, strlen(buff1));
printf("send information:%s\n", buff1);
bzero(buff2, sizeof(buff2));
read(fd[0], buff2, sizeof(buff2));
printf("received information:%s\n", buff2);
return 0;
}
2.3、多进程使用管道进行通信
注意:创建管道之后,再创建子进程,此时一共有4个文件描述符。
4个端口,父子进程分别有一个读端口和一个写端口
向任意一个写端口写数据,即可从任意一个读端口获取数据。
实例:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
int fd[2];
int ret;
char buff1[1024];
char buff2[1024];
pid_t pd;
ret = pipe(fd);
if (ret !=0) {
printf("create pipe failed!\n");
exit(1);
}
pd = fork();
if (pd == -1) {
printf("fork error!\n");
exit(1);
} else if (pd == 0) {
bzero(buff2, sizeof(buff2));
read(fd[0], buff2, sizeof(buff2));
printf("process(%d) received information:%s\n", getpid(), buff2);
} else {
strcpy(buff1, "Hello!");
write(fd[1], buff1, strlen(buff1));
printf("process(%d) send information:%s\n", getpid(), buff1);
}
if (pd > 0) {
wait();
}
return 0;
}
2.4、子进程使用exec启动新程序时管道的使用
有程序P1, P2
使用管道进行通信
P1由用户输入一个字符串,然后把该字符串发给p2
P2接收到以后,把该字符串打印出来
P1:
创建管道
创建子进程
在子进程中用exec替换成p2,
(在使用exec 时,把管道的读端作为exec的参数)
在父进程中,获取用户的输入,然后把所输入的字符串发送给p2
(即,父进程把字符串写入管道)
P2:
从参数中获取管道的读端(参数即为p2的main函数的参数)
读管道
把读到的字符串打印出来
难点:子进程使用exec启动新程序运行后,
新进程能够使用原来子进程的管道(因为exec能共享原来的文件描述符)
但问题是新进程并不知道原来的文件描述符是多少!
解决方案:
把子进程中的管道文件描述符,用exec的参数传递给新进程。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
int fd[2];
int ret;
char buff1[1024];
char buff2[1024];
pid_t pd;
ret = pipe(fd);
if (ret !=0) {
printf("create pipe failed!\n");
exit(1);
}
pd = fork();
if (pd == -1) {
printf("fork error!\n");
exit(1);
} else if (pd == 0) {
//bzero(buff2, sizeof(buff2));
sprintf(buff2, "%d", fd[0]);
execl("main3_2", "main3_2", buff2, 0);
printf("execl error!\n");
exit(1);
} else {
strcpy(buff1, "Hello!");
write(fd[1], buff1, strlen(buff1));
printf("process(%d) send information:%s\n", getpid(), buff1);
}
if (pd > 0) {
wait();
}
return 0;
}
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
int fd;
char buff[1024] = {0,};
sscanf(argv[1], "%d", &fd);
read(fd, buff, sizeof(buff));
printf("Process(%d) received information:%s\n", getpid(), buff);
return 0;
}
2.5、关闭管道的读端/写端
管道关闭后的读操作:
问题:
对管道进行read时,如果管道中已经没有数据了,此时读操作将被“阻塞”。
如果此时管道的写端已经被close了,则写操作将可能被一直阻塞!
而此时的阻塞已经没有任何意义了。(因为管道的写端已经被关闭,即不会再写入数据了)
解决方案:
如果不准备再向管道写入数据,则把该管道的所有写端都关闭,
则,此时再对该管道read时,就会返回0,而不再阻塞该读操作。(管道的特性)
注意,这是管道的特性。
如果有多个写端口,而只关闭了一个写端,那么无数据时读操作仍将被阻塞。
实际实现方式:
父子进程各有一个管道的读端和写端;
把父进程的读端(或写端)关闭;
把子进程的写端(或读端)关闭;
使这个“4端口”管道变成单向的“2端口”管道,如图:
实例:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
int fd[2];
int ret;
char buff1[1024];
char buff2[1024];
pid_t pd;
ret = pipe(fd);
if (ret !=0) {
printf("create pipe failed!\n");
exit(1);
}
pd = fork();
if (pd == -1) {
printf("fork error!\n");
exit(1);
} else if (pd == 0) {
close(fd[1]);
bzero(buff2, sizeof(buff2));
read(fd[0], buff2, sizeof(buff2));
printf("process(%d) received information:%s\n", getpid(), buff2);
} else {
strcpy(buff1, "Hello!");
close (fd[0]);
write(fd[1], buff1, strlen(buff1));
printf("process(%d) send information:%s\n", getpid(), buff1);
close (fd[1]);
}
if (pd > 0) {
wait();
}
return 0;
}
把父进程的写操作注释掉,此时子进程的读操作将被一直阻塞
把父进程的写操作注释掉,并close父进程的写端
此时子进程的读操作将将被阻塞。
把父进程的写操作注释掉,并把父子进程的写端都close
此时子进程读操作将直接返回0,而不再阻塞。
最终实现方案:
关闭父进程的读端,关闭子进程的写端。
当父进程不再发送数据时,就关闭本进程的写端。
2.6、练习
创建一个子进程
父进程:
循环等待用户输入,
用户每输入一个单词后,就把该单词用管道发送给子进程,
直到用户输入exit。
子进程:
每收到一个单词后,就打印输出
直到用户在父进程中结束输入。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define CMD_EXIT "exit"
int main(void)
{
int fd[2];
int ret;
char buff[80];
pid_t pd;
ret = pipe(fd);
if (ret !=0) {
printf("create pipe failed!\n");
exit(1);
}
pd = fork();
if (pd == -1) {
printf("fork error!\n");
exit(1);
} else if (pd == 0) {
close(fd[1]);
while (1) {
bzero(buff, sizeof(buff));
if (read(fd[0], buff, sizeof(buff)) == 0) {
break;
}
printf("received information:%s\n", buff);
}
printf("receive end!\n");
close(fd[0]);
} else {
close (fd[0]);
while(1) {
scanf("%s", buff);
if (strcmp(buff, CMD_EXIT) == 0) {
break;
}
write(fd[1], buff, strlen(buff));
}
close (fd[1]);
}
if (pd > 0) {
wait();
}
return 0;
}
2.7、把管道作为标准输入和标准输出
把管道作为标准输入和标准输出的优点:
子进程使用exec启动新程序时,就不需要再把管道的文件描述符传递给新程序了。
可以直接使用使用标准输入(或标准输出)的程序。
比如 od –c (统计字符个数,结果为八进制)
实现原理:
使用dup复制文件描述符
用exec启动新程序后,原进程中已打开的文件描述符仍保持打开,即可以共享原进程中的文件描述符。
注意:dup的用法
dup复制文件描述符,
返回的新文件描述符和被复制的文件描述符,指向同一个文件或管道
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
int fd[2];
int ret;
char buff1[1024];
char buff2[1024];
pid_t pd;
ret = pipe(fd);
if (ret !=0) {
printf("create pipe failed!\n");
exit(1);
}
pd = fork();
if (pd == -1) {
printf("fork error!\n");
exit(1);
} else if (pd == 0) {
//bzero(buff2, sizeof(buff2));
//sprintf(buff2, "%d", fd[0]);
close(fd[1]);
close(0);
dup(fd[0]);
close(fd[0]);
execlp("./od.exe", "./od.exe", "-c", 0);
printf("execl error!\n");
exit(1);
} else {
close(fd[0]);
strcpy(buff1, "Hello!");
write(fd[1], buff1, strlen(buff1));
close(fd[1]);
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
int main(void)
{ int ret = 0;
char buff[80] = {0,};
ret = scanf("%s", buff);
printf("[ret: %d]buff=%s\n", ret, buff);
ret = scanf("%s", buff);
printf("[ret: %d]buff=%s\n", ret, buff);
return 0;
}
2.8、使用popen/pclose
popen的作用:
用来在两个程序之间传递数据:
在程序A中使用popen调用程序B时,有两种用法:
程序A读取程序B的输出(使用fread读取)
程序A发送数据给程序B,以作为程序B的标准输入。(使用fwrite写入)
用法:man popen
返回值:成功,返回FILE*
失败, 返回空
实例1:
#include <stdio.h>
#include <stdlib.h>
#define BUFF_SIZE 1024
int main(void)
{
FILE * file;
char buff[BUFF_SIZE+1];
int cnt;
// system("ls -l > result.txt");
file = popen("ls -l", "r");
if (!file) {
printf("fopen failed!\n");
exit(1);
}
cnt = fread(buff, sizeof(char), BUFF_SIZE, file);
if (cnt > 0) {
buff[cnt] = '\0';
printf("%s", buff);
}
pclose(file);
return 0;
}
实例2:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUFF_SIZE 1024
int main(void)
{
FILE * file;
char buff[BUFF_SIZE+1];
int cnt;
file = popen("./p2", "w");
if (!file) {
printf("fopen failed!\n");
exit(1);
}
strcpy(buff, "hello world!");
cnt = fwrite(buff, sizeof(char), strlen(buff), file);
pclose(file);
return 0;
}
popen的原理:
先使用fork创建一个子进程,
然后在子进程中使用exec执行指定外部程序,并返回一个文件指针FILE*给父进程。
当使用”r”时,该FILE指向外部程序的标准输出
当使用”w”时,该FILE指向外部程序的标准输入。
popen的优缺点:
优点:可以使用shell扩展(比如命令中可以使用通配符)
使用方便。
缺点:每调用一次popen, 将要启动两个进程(shell和被指定的程序),
资源消耗大。
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE
3、消息队列
3.1、什么是消息队列
消息队列,用于从一个进程向另一个进程发送数据。
但仅把数据发送到一个“队列”中,而不指定由哪个进程来接受。
消息队列,独立与发送消息的进程和接收消息的进程。
(信号、管道、命名管道都不独立与发送和接收进程)
消息队列,有最大长度限制:MSGMNB
消息队列中的单条消息,也有最大长度限制:MSGMAX
3.2、消息队列的获取
msgget
原型:int msgget(key_t key, int msgflg);
功能:获取或创建一个消息队列
参数:与共享内存相似。
msgflag可使用IPC_CREAT
返回值:成功,返回正整数,即“消息队列标识符”
失败,返回-1
3.3、消息队列的发送
msgsnd
原型: int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:发送一个消息,即把消息添加到消息队列中
参数:msgid 消息队列标识符
msgp 消息指针
注:消息的类型需要自己定义。但要求其第一个结构成员为long int
例: struct my_msg_st
{
long int msg_type; /* 消息的类型,取>0, 接收消息时可使用该值 */
/*other info */
}
msgsz 消息的长度(不包含第一个成员msg_type)
msgflg 如果包含: IPC_NOWAIT, 则消息队列满时,不发送该消息,而立即返回-1
如果不包含:IPC_NOWAIT,则消息队列满时,挂起本进程,直到消息队列有空间可用。
返回值:成功,返回0
失败,返回-1
3.4、消息的接收
msgrcv
原型: ssize_t msgrcv (int msqid, void *msgp, size_t msgsz, long msgtype, int msgflg);
功能:从消息队列中接收一条消息。
参数:msgid 消息队列标识符
msgp 用于接收消息的缓存
msgsz 要接收的消息的长度(不包括其第一个成员)
msgtype 指定接收消息的类型
0: 从消息队列中获取第一个消息,以实现顺序接受(先发先收)
>0: 从消队列中获取相同类型的第一个消息
<0: 从消息队列中获取消息类型<=(msgtyep的绝对值)的第一个消息
msgflg: 如果包含 IPC_NOWAIT, 则当消息队列中没有指定类型的消息时,立即返回-1
如果不包含:IPC_NOWAIT,则当消息队列中没有指定类型的消息时,挂起本进程,直到收到指定类型的消息
返回值:成功,返回接收到的消息的长度(不包含第一个成员msg_type)
失败,返回-1
3.5、消息的控制
msgctl
原型:int msgctl(int msqid, int cmd, struct msqid_ds *buf);
功能:与shmctl类似
参数:cmd 常用命令:
IPC_RMID 删除消息队列
返回值:成功, 返回 0
失败,返回-1
3.6、实例
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MSG_SIZE 80
struct my_msg_st {
long int msg_type;
char msg[MSG_SIZE];
};
int main(void)
{
int msgid;
int ret;
struct my_msg_st msg;
msgid = msgget((key_t)1235, 0666|IPC_CREAT);
if (msgid == -1) {
printf("msgget failed!\n");
exit(1);
}
msg.msg_type = 1;
strcpy(msg.msg, "Hello world!");
ret = msgsnd(msgid, &msg, MSG_SIZE, 0);
if (ret == -1) {
printf("msgsnd failed!\n");
exit(1);
}
return 0;
}
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#define MSG_SIZE 80
struct my_msg_st {
long int msg_type;
char msg[MSG_SIZE];
};
int main(void)
{
int msgid;
int ret;
struct my_msg_st msg;
msgid = msgget((key_t)1235, 0666|IPC_CREAT);
if (msgid == -1) {
printf("msgget failed!\n");
exit(1);
}
msg.msg_type = 0;
ret = msgrcv(msgid, &msg, MSG_SIZE, 0, 0);
if (ret == -1) {
printf("msgrcv failed!\n");
exit(1);
}
printf("received: %s\n", msg.msg);
ret = msgctl(msgid, IPC_RMID, 0);
if (ret == -1) {
printf("msgctl(IPC_RMID) failed!\n");
exit(1);
}
return 0;
}
3.7、练习
程序1, 循环等待用户输入字符串,
每收到一个字符串,就把它发送给进程2
直到用户输入exit
程序2, 接受进程1发过来的信息,并打印输出。
直到接受到exit。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MSG_SIZE 80
struct my_msg_st {
long int msg_type;
char msg[MSG_SIZE];
};
int main(void)
{
int msgid;
int ret;
struct my_msg_st msg;
msgid = msgget((key_t)1235, 0666|IPC_CREAT);
if (msgid == -1) {
printf("msgget failed!\n");
exit(1);
}
while(1) {
fgets(msg.msg, sizeof(msg.msg), stdin);
msg.msg_type = 1;
ret = msgsnd(msgid, &msg, MSG_SIZE, 0);
if (ret == -1) {
printf("msgsnd failed!\n");
exit(1);
}
if (strncmp(msg.msg, "exit", 4) == 0) {
break;
}
}
return 0;
}
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#define MSG_SIZE 80
struct my_msg_st {
long int msg_type;
char msg[MSG_SIZE];
};
int main(void)
{
int msgid;
int ret;
struct my_msg_st msg;
msgid = msgget((key_t)1235, 0666|IPC_CREAT);
if (msgid == -1) {
printf("msgget failed!\n");
exit(1);
}
while(1) {
msg.msg_type = 0;
ret = msgrcv(msgid, &msg, MSG_SIZE, 0, 0);
if (ret == -1) {
printf("msgrcv failed!\n");
exit(1);
}
printf("received: %s\n", msg.msg);
if (strncmp(msg.msg, "exit", 4) == 0) {
break;
}
}
ret = msgctl(msgid, IPC_RMID, 0);
if (ret == -1) {
printf("msgctl(IPC_RMID) failed!\n");
exit(1);
}
return 0;
}
4、信号量
问题
程序中,有时存在一种特殊代码,最多只允许一个进程执行该部分代码。这部分区域,称为“临界区”
然而在多进程并发执行时,当一个进程进入临界区,因某种原因被挂起时,其他进程就有可能也进入该区域。
解决办法:使用信号量
4.1、什么是信号量
信号量,是一种特殊的变量。
只能对信号量执行P操作和V操作
P操作, 如果信号量的值 > 0, 则把该信号量减1
如果信号量的值 ==0, 则挂起该进程。
V操作: 如果有进程因该信号量而被挂起,则恢复该进程运行
如果没有进程因该信号量而挂起,则把该信号量加1
注意:P操作、V操作都是原子操作,即其在执行时,不会被中断。
注意:此指的“信号量”是指System V IPC的信号量,与线程所使用的信号量不同。该信号量,用于进程间通信。
4.2、信号量的使用
1)信号量的获取
semget
原型:int semget(key_t key, int nsems, int semflg);
功能:获取一个已存在的、或创建一个新的信号量量,返回该信号量的标识符
参数:key, 键值,该键值对应一个唯一的信号量。类似于共享内存的键值。
不同的进程可通过该键值和semget获取唯一的信号量。特殊键值:IPC_PRIVAT该信号量只允许创建者本身, 可用于父子进程间通信。
nsems, 需要的信号量数目,一般取1
sem_flags, 与共享内存的sem_flags类似。IPC_CREAT, 如果该信号量未存在,则创建该信号量如果该信号量已存在,也不发送错误。
返回值: 成功,则返回一个正数
失败, 返回返回-1
2) 信号量的操作
semop
原型:int semop(int semid, struct sembuf *sops, unsigned nsops);
功能:改变信号量的值,即对信号量执行P操作、或V操作。
参数:semid, 信号量标识符, 即semget的返回值
sops, 是一个数组,元素类型为struct sembuf
struct sembuf {
short sem_num; //信号量组中的编号(即指定对哪个信号量操作)
//semget实际是获取一组信号量
//如果只获取了一个信号量,则该成员取0
short sem_op; // -1, 表示P操作
// 1, 表示V操作
short sem_flg; // SEM_UNDO : 如果进程在终止时,没有释放信号量
// 如果不设置指定标志,应该设置为0 则,自动释放该信号量
}
nsops, 表示第二个参数sops所表示的数组的大小,即表示有几个struct sembuf
返回值: 失败, 返回-1
成功, 返回 0
3) 信号量的控制
semctl
原型:int semctl(int semid, int sem_num, int cmd, ...);
功能:对信号量进行控制
参数:semid, 信号量标识符
sem_num, 信号量组中的编号,如果只有一个信号量,则取0
cmd, SETVAL 把信号量初始化为指定的值,具体的值由第4个参数确定
注意:只能对信号量初始化一次,如果在各进程中,分别对该信号量进行初始化,则可能导致错误!
IPC_RMID 删除信号量
参数4, 类型为:union semun {
int val; // SETVAL命令要设置的值
struct semid_ds *buf;
unsigned short *array;
}
注意:union semun类型要求自己定义有些Linux发行版在sys/sem.h中定义,有些发行版则没有定义。
可自定义如下:
-------------------------------------------------------------------------------------
#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
#else
union semun {
int val;
struct semid_ds *buf;
unsigned short int *array;
struct seminfo *__buf;
};
#endif
4.3、实例
实例1:不使用信号量,并发执行多个进程,观察对临界区的访问。
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
int i;
pid_t pd = fork();
for (i=0; i<5; i++) {
/* 模拟临界区----begin */
printf("Process(%d) In\n", getpid());
sleep(1);
printf("Process(%d) Out\n", getpid());
/* 模拟临界区----end */
sleep(1);
}
return 0;
}
实例2:使用信号量,并发执行多个进程,观察对临界区的访问。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
#include <stdio.h>
#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
#else
union semun {
int val;
struct semid_ds *buf;
unsigned short int *array;
struct seminfo *__buf;
};
#endif
static sem_initial(int semid)
{
int ret;
union semun semun;
semun.val = 1;
ret = semctl(semid, 0, SETVAL, semun);
if (ret == -1) {
fprintf(stderr, "semctl failed!\n");
}
return ret;
}
static int sem_p(int semid)
{
int ret;
struct sembuf sembuf;
sembuf.sem_op = -1;
sembuf.sem_num = 0;
sembuf.sem_flg = SEM_UNDO;
ret = semop(semid, &sembuf, 1);
if (ret == -1) {
fprintf(stderr, "sem_p failed!\n");
}
return ret;
}
static int sem_v(int semid)
{
int ret;
struct sembuf sembuf;
sembuf.sem_op = 1;
sembuf.sem_num = 0;
sembuf.sem_flg = SEM_UNDO;
ret = semop(semid, &sembuf, 1);
if (ret == -1) {
fprintf(stderr, "sem_v failed!\n");
}
return ret;
}
int main(int argc, char* argv[])
{
int i;
int ret;
int semid;
/* 获取信号量 */
semid = semget((key_t)1234, 1, 0666 | IPC_CREAT);
if (semid == -1) {
printf("semget failed!\n");
exit(1);
}
/* 初始化信号量 */
if (argc > 1) {
ret = sem_initial(semid);
if (ret == -1) {
exit(1);
}
}
for (i=0; i<5; i++) {
if (sem_p(semid) == -1) {
exit(1);
}
/* 模拟临界区----begin */
printf("Process(%d) In\n", getpid());
sleep(1);
printf("Process(%d) Out\n", getpid());
/* 模拟临界区----end */
if (sem_v(semid) == -1) {
exit(1);
}
sleep(1);
}
/* 删除信号量 */
return 0;
}
5、共享内存机制
是允许两个或多个进程(不相关或有亲缘关系)访问同一个逻辑内存的机制。它是共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。
5.1、两种常用共享内存方式
System V版本的共享内存 shmm
多个进程直接共享内存
文件映射 mmap
文件进行频繁读写,将一个普通文件映射到内存中
将特殊文件进行匿名内存映射,为关联进程提供共享内存空间
为无关联的进程提供共享内存空间,将一个普通文件映射到内存中
ftok函数生成key标识符
key_t ftok(const char *pathname,int proj_id)
创建一个共享内存块,返回这个共享内存块的标识符shmid
int shmget(key_t key,size_t size,int shmflg)
参数说明:size - 申请的共享内存的大小,为4k的整数倍;
shmflg - IPC_CREAT 创建新的共享内存,已存在 使用IPC_EXCL
挂接共享内存(将进程地址空间挂接到物理空间,可以有多个挂接)
void *shmat(int shmid,const void *shmaddr, int shmflg)
参数说明:shmid - 挂接的共享内存ID.
shmaddr - 一般为0,表示连接到由内核选择的第一个可用地址上
shmflg - 一般为0
取消共享内存映射
int shmdt(const void *shmaddr);
用于控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数 shmid - 由shmget返回的共享内存标识码
cmd - 将要采取的动作(可取值:IPC_STAT、IPC_SET、IPC_RMID)
buf - 指向一个保存着共享内存的模式状态和访问权限的数据结构
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/shm.h>
struct Conn_stat
{
int count;
char ip[64];
};
int main()
{
void *shm = NULL;
int shmid = 0, i = 0;
struct Conn_stat stat = {0,"127.0.0.1"};
//创建共享内存
shmid = shmget((key_t)1234, sizeof(struct Conn_stat), 0666|IPC_CREAT);
if(shmid == -1)
{
fprintf(stderr, "shmget failed\n");
exit(1);
}
//将共享内存连接到当前进程的地址空间
shm = shmat(shmid, (void*)0, 0);
if(shm == (void*)-1)
{
fprintf(stderr, "shmat failed\n");
exit(2);
}
printf("Memory attached at %p\n", shm);
//设置共享内存
struct Conn_stat *p = (struct Conn_stat*)shm;
memcpy(p,&stat,sizeof(struct Conn_stat));
while((i++) < 30)//修改共享内存中写数据
{
p->count++;
sleep(1);
}
//把共享内存从当前进程中分离
if(shmdt(shm) == -1)
{
fprintf(stderr, "shmdt failed\n");
exit(3);
}
exit(0);
}
//shmread.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
#include <errno.h>
struct Conn_stat
{
int count;
char ip[64];
};
int main()
{
void *shm = NULL;//分配的共享内存的原始首地址
struct Conn_stat *stat = NULL;//指向shm
int shmid;//共享内存标识符
//创建共享内存
shmid = shmget((key_t)1234, sizeof(struct Conn_stat), 0666|IPC_CREAT);
if(shmid == -1)
{
fprintf(stderr, "shmget failed\n");
exit(0);
}
//将共享内存连接到当前进程的地址空间
shm = shmat(shmid, 0, 0);
if(shm == (void*)-1)
{
fprintf(stderr, "shmat failed\n");
exit(1);
}
printf("\nMemory attached at %p\n", shm);
//设置共享内存
stat = (struct Conn_stat*)shm;
int i = 0;
while((i++) < 10)
{
printf("ip = %s ,count: %d\t\t\n", stat->ip, stat->count);
sleep(1);
}
//把共享内存从当前进程中分离
if(shmdt(shm) == -1)
{
fprintf(stderr, "shmdt failed\n");
exit(2);
}
//删除共享内存
if(shmctl(shmid, IPC_RMID, 0) == -1)
{
fprintf(stderr, "shmctl(IPC_RMID) failed, reason: %s\n",strerror(errno));
exit(3);
}
exit(0);
}
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);
参数addr:指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选
定地址,映射成功后返回该地址。
参数length:代表将文件中多大的部分映射到内存。
参数prot:映射区域的保护方式。可以为以下几种方式的组合:
PROT_EXEC 执行
PROT_READ 读取
PROT_WRITE 写入
PROT_NONE 不能存取
参数flags:影响映射区域的各种特性。必须要指定MAP_SHARED 或MAP_PRIVATE。
MAP_SHARED - 映射区域数据与文件对应,允许其他进程共享
MAP_PRIVATE - 映射区域生成文件的copy,修改不同步文件
MAP_ANONYMOUS - 建立匿名映射。此时会忽略参数fd,不涉及文件,而
且映射区域无法和其他进程共享。
MAP_DENYWRITE - 允许对映射区域的写入操作,其他对文件直接写入的
操作将会被拒绝。
MAP_LOCKED - 将映射区域锁定住,这表示该区域不会被置swap
参数fd:要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,然后对该文件进行映射,可以同样达到匿名内存映射的效果。
参数offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<string.h>
struct Conn_stat
{
int count;
char ip[64];
};
int main(int argc,char *argv[]) //这个进程用于创建映射区进行写。
{
if(argc != 2)
{
printf("Usage: %s file.\n",argv[0]);
exit(1);
}
struct Conn_stat stat = {0,"127.0.0.1"};
int fd = open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd < 0)
{
perror("open");
exit(2);
}
ftruncate(fd,sizeof(struct Conn_stat));
struct Conn_stat *p = (struct Conn_stat*)mmap(NULL,sizeof(struct Conn_stat),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);//创建一个结构体大小的共享映射区。共享映射区我们可以当做数组区看待。
if(p == MAP_FAILED)
{
perror("mmap");
exit(3);
}
close(fd); //关闭不用的文件描述符。
memcpy(p,&stat,sizeof(struct Conn_stat));
while(p->count < 30)
{
p->count++;
sleep(1);
}
int ret = munmap(p,sizeof(struct Conn_stat));
if(ret < 0)
{
perror("mmumap");
exit(4);
}
return 0;
}
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/mman.h>
struct Conn_stat
{
int count;
char ip[64];
};
int main(int argc,char *argv[]) //这个进程创建映射区进行读
{
if(argc != 2)
{
printf("Usage: %s file.\n",argv[0]);
exit(1);
}
int fd = open(argv[1],O_RDONLY,0644);
if(fd < 0)
{
perror("open");
exit(2);
}
struct Conn_stat stat;
struct Conn_stat *p = (struct Conn_stat*)mmap(NULL,sizeof(struct Conn_stat),PROT_READ,MAP_SHARED,fd,0);
if(p == MAP_FAILED)
{
perror("mmap");
exit(3);
}
close(fd);
int i = 0;
while((i++) < 10)
{
printf("ip = %s ,count: %d\t\t\n",p->ip,p->count);
sleep(1);
}
int ret = munmap(p,sizeof(stat));
if(ret < 0)
{
perror("mmumap");
exit(4);
}
return 0;
}