文件编程概述

计算机如何帮助我们自动化完成以上操作
操作系统提供了一系列的API
如Linux系统:

1.在Linux中要操作一个文件,一般是先open打开一个文件,得到文件描述符,然后对文件进行读写操作(或其它操作),最后close关闭文件即可。

2.强调一点:我们对文件进行操作时,一定要先打开文件,打开成功后才能操作,如果打开失败,就不用进行后面的操作了,最后读写完成后,一定要关闭文件,否则会造成文件损坏。

3.文件平时是存放在块设备中的文件系统文件中的,我们把这种文件叫静态文件,当我们去open打开一个文件时,Linux内核做的操作包括:内核在进程中建立一个打开文件的数据结构,记录下我们打开的这个文件;内核在内存中申请一段内存,并且将静态文件的内容从块设备读取到内核中特地地址管理存放(叫动态文件)。

4.打开文件以后,以后对这个文件的读写操作,都是针对内存中的这一份动态文件的,而并不是针对静态文件的。当我们对动态文件进行读写以后,此时内存中动态文件和块设备文件中的静态文件就不同步了,当我们close关闭动态文件时,close内部内核将内存中的动态文件的内容去更新(同步)块设备中的静态文件。

5.为什么这么设计,不直接对块设备直接操作
块设备本身读写非常不灵活,是按块 读写的,而内存是按字节单位操作的,而且可以随机操作,很灵活。

打开 open
读写 write/read
光标定位 lseek
关闭 close

文件描述符:
1.对于内核而言,所有打开文件都由文件描述符引用。文件描述符是一个非负整数。当打开一个现存或者创建一个新文件时,内核向进程返回一个文件描述符。当读写一个文件时,用open和creat返回的文件描述符标识该文件,将其作为参数传递给read和write。

2.文件描述符,这个数字在一个进程中表示一个特定含义,当我们open一个文件时,操作系统在内存中构建了一些数据结构来表示这个动态文件,然后返回给应用程序一个数字作为文件描述符,这个数字就和我们内存中维护的这个动态文件的这些数据结构绑上了,以后我们应用程序如果要操作这个动态文件,只需要用这个文件描述符区分。

3.文件描述符的作用域就是当前进程,出了这个进程文件描述符就没有意义了。

标准输入:0
标准输出:1
标准错误:2

demo:

read(0,readBuf,5);
write(1,readBuf,strlen(readBuf));

打开/创建文件
函数原型

int open(const char *pathname,int flags,mode_t mode);

pathname:要打开的文件名(含路径,缺省为当前路径)
flags:O_RDONLY 只读打开 O_WRONLY 只写打开 O_RDWR 可读可写
mode:一定是在flags中使用了O_CREAT,mode记录创建的文件的访问权限,如666

demo:

int main()
{
	int fd; //文件描述符

	fd = open("./file1",O_RDWR|O_CREAT,0666);
	if(fd == -1)
	{
		printf("open failed\n");
		perror("open");
	}else if(fd > 0){
		printf("fd = %d\n",fd);
	}

	return 0;
}

写入文件
函数原型

ssize_t write(int fd,const void *buf,size_t count);

fd:文件描述符
buf:缓冲,需要写入的数据
count:数据的大小

demo:

int main()
{
	int fd;
	char *buf = "helloworld";
	fd = open("./file1",O_RDWR|O_CREAT,0666);
	write(fd,buf,strlen(buf));   //计算字符串大小用strlen不能用sizeof
	close(fd);
}

读取文件(read)
函数原型

ssize_t read(int fd,void *buf,size_t count);

fd:文件描述符
buf:缓冲,需要写入的数据
count:数据的大小

demo:

int main()
{
	int fd;
	int n_write,n_read;
	char *readBuf;
	char *buf = "helloworld";
	fd = open("./file1",O_RDWR|O_CREAT,0666);
	n_write = write(fd,buf,strlen(buf));   //计算字符串大小用strlen不能用sizeof

	readBuf = (char *)malloc(sizeof(char)*n_write+1);
	n_read = read(fd,readBuf,128);
	printf("read: %s\n",readBuf);

	return 0;
}

光标移动
函数原型

off_t lseek(int fd,off_t offset,int whence);

fd:文件描述符
offset:偏移量
whence: SEEK_SET 指向文件头
SEEK_END 指向文件尾
SEEK_CUR 指向当前位置

自己实现CP指令 demo

int main(int argc,char *argv[])
{
	
	int fd1;
	int fd2;
	int n_read
	int n_lseek;
	char *readBuf;

	fd1 = open(argv[1],O_RDWR);
	n_lseek = lseek(fd1,0,SEEK_END);

	lseek(fd1,0,SEEK_SET);
	readBuf = malloc(sizeof(char)*n_lseek+8);
	n_read = read(fd1,readBuf,n_lseek);

	fd2 = open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0600);
	write(fd2,readBuf,strlen(readBuf));

	close(fd1);
	close(fd2);

	return 0;
}

配置文件的修改

LENG=3;

步骤
1.找到要修改的位置
2.指针往后移
3.找到后进行修改

demo

int main()
{
	char *p;
	p = strstr(readBuf,"LENG=");
	if(p == NULL)
	{
		printf("not found\n");
		exit(-1);
	}else{
		p = p+strlen("LENG=");
		*p = '5';
	}

	return 0;
}

open和fopen的区别

1.来源
从来源的角度看,俩者能很好的分开,这也是俩者最显而易见的区别:
open是UNIX系统调用函数(包括Linux等),返回的是文件描述符,它是文件在文件描述符表里的索引。
fopen是ANSIC标准的C语言库函数,在不同的系统中应该调用不同的内核api。返回的是一个指向文件结构的指针。

2.移植性
这一点从上面的来源就可以推断出来,fopen是C标准函数,因此拥有良好的移植性;而open是UNIX系统调用,移植有限,如windows相似的功能使用API函数CreateFile。

3.适用范围
oepn返回文件描述符,而文件描述符unix系统下的一个重要概念,打开驱动文件要用open
fopen用来操作普通文件。

4.文件的IO层次

5.缓冲
1.fopen在缓冲区 fwrite fread配合使用
2.open在非缓冲区 write read配合使用

fopen
原型

FILE *fopen(char *filename,char *mode);
size_t fwrite(const void *ptr,size_t size,size_t nmemb,FILE *stream);
size_t fread(void *ptr,size_t size,size_t nmemb,FILE *stream);
int fseek(FILE *stream,long offset,int whence);

例子:

int main()
{
	char *buf = "hello";
	FILE *fp;
	char readBuf[128] = {'\0'};

	fp = fopen("./test.txt","w+");

	fwrite(buf,sizeof(buf),strlen(buf),fp);

	fseek(fp,0,SEEK_SET);

	fread(readBuf,sizeof(buf),strlen(buf),fp);
	printf("read: %s\n",readBuf);

	return 0;
}
int fputs(int c,FILE *stream);  //往文件写入一个整数
int fputs(const char *s,FILE *stream);  //往文件写入一个字符

int feof(FILE *stream);//判断是否到文件尾部  到结尾返回0

进程

1.什么是程序,什么是进程,有什么区别

程序是静态的概念,gcc xxx.c -o xxx
磁盘中生成xxx文件,叫做程序(静态)

进程是程序的一次运行活动,通俗点意思就是程序跑起来了,系统中就多了一个进程(动态)

2.如何查看系统中有哪些进程
用ps指令

3.什么是进程标识符?
每个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证。

getpid(); //获取子进程的pid号
getppid();

4.什么叫父进程,什么叫子进程
进程A创建了进程B
那么A叫父进程,B叫做子进程,父子进程是相对的概念,理解为人类中的父子关系。

5.C程序的存储空间是如何分配

在这里插入图片描述

正文:代码段
初始化数据段,也称数据段,如int maxcount = 99;
未初始化数据段,也称bss段 如: long sum[1000];
栈:自动变量以及每次函数调用时所需保存的信息
堆:malloc动态分配的空间。

进程创建
使用fork函数创建一个进程

pid_t fork(void);

fork函数调用成功,返回俩次
返回值为0,代表当前进程是子进程
返回值非负数,代表当前进程为父进程

调用失败,返回-1

例子

int main()
{
	pid_t pid;

	pid = fork();

	if(pid == 0)
	{
		printf("This is child\n",getpid());
	}else if(pid > 0)
	{
		printf("This is father\n",getpid());
	}

	return 0;
}

fork创建一个子进程的一般目的
(1)一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的— 父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
(2)一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。

vfork函数

vfork 也可以创建进程,与fork有什么区别

关键区别一:
vfork直接使用父进程存储空间,不拷贝。
关键区别二:
vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行。

例子

int main
{
	int cnt;
	pid_t pid;

	pid = vfork();

	if( pid > 0)
	{
		while(1)
		{
			printf("this is father\n");
			sleep(1);
			printf("cnt = %d\n",cnt);
		}
		
	}else if(pid == 0){
		printf("this is child\n");
		sleep(1);
		cnt ++;
		if(cnt == 3)
		{
			exit(0);   //子进程退出
		}
	}


	return 0;
}

进程退出

正常退出
1.Main函数调用return
2.进程调用exit();标准c库
3.进程调用_exit()或者_Exit(),属于系统调用

异常退出
1.调用abort
2.当进程收到某些信号时,如ctrl+C
3.最后一个线程对取消请求做出响应

补充:
1.进程最后一个线程返回
2.最后一个线程调用pthread_exit

为什么要等待子进程退出
子进程退出状态不被收集,变成僵尸进程

原型

pid_t wait(int *status);  
pid_t waitpid(pid_t pid,int *status,int options);

wait和waitpid的区别

wait使使用者阻塞,waitpid有一个选项,可以使调用者不阻塞。

如果其所有子进程都还在运行,则阻塞。
如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
如果它没有任何子进程,则立即出错返回。

例子

int main
{
	int cnt;
	pid_t pid;
	int status = 10; 
	pid = fork();

	if( pid > 0)
	{
		wait(&status);  //等待子进程退出,阻塞
		printf("status = %d",WEXITSTATUS(status));
		while(1)
		{
			printf("this is father\n");
			sleep(1);
			printf("cnt = %d\n",cnt);
		}
		
	}else if(pid == 0){
		printf("this is child\n");
		sleep(1);
		cnt ++;
		if(cnt == 3)
		{
			exit(2);   //子进程退出
		}
	}


	return 0;
}

什么孤儿进程

父进程如果不等待子进程退出,在子进程之前就结束了自己的"生命",此时子进程叫做孤儿进程
Linux避免系统存在过多的孤儿进程,Init进程收留孤儿进程(进程ID为1),变成孤儿进程的父进程

exec族函数
作用:我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。
功能:在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。

例子

int main()
{
	execl("/bin/ls","ls","-l","NULL");
	return 0;
}

system

函数原型

int system(const char *command);

int main()
{
	system("ls");
	return 0;
}

popen

原型
**FILE *popen(const char command,const char type);

例子

int main()
{
	char buf[128] = {0};
	FILE *p;
	p = popen("ps","r");

	fread(buf,sizeof(buf),1,p);

	printf("reg = %s\n",buf);


	return 0;
}

进程间通信(IPC)

IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享内存、Socket、Streams等。其中Socket和Stream支持不同主机上的俩个进程IPC。

1.管道
管道,通常指无名管道,是UNIX系统IPC最古老的形式。

特点:
1.它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。管道数据,读走就没了。

2.它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。

3.它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

原型:

int pipe(int fd[2]);    // 返回值:若成功返回0,失败返回-1
当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。
int main()
{
	pid_t pid;
	char buf[128] = {0}; 

	if(pipe() == -1);
	{
		printf("pipe creat failed\n");
	}

	pid = fork();
	if(pid == 0)        //子进程
	{
		printf("this is child\n");
		close(fd[1])
		read(fd[0],buf,sizeof(buf));
		printf("read from pipe:%s\n",buf);
	}else if(pid > 0)   //父进程
	{
		printf("this is father\n");
		close(fd[0]);
		write(fd[1],"helloworld",strlen("helloworld"));
	}
	return 0;
}

FIFO
FIFO,也称为命名管道,它是一种文件类型。

1、特点

FIFO可以在无关的进程之间交换数据,与无名管道不同。

FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

2、原型

int mkfifo(const char *pathname, mode_t mode);  // 返回值:成功返回0,出错返回-1
其中的 mode 参数与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。

当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:

若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。

若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。

例子
fifo.read

int main()
{
	int fd;
	char buf[128] = {0};

	fd = open("./file",O_RDONLY);
	read(fd,buf,sizeof(buf));

	printf("read from fifo:%s\n",buf);

	return 0;
}

fifo.write

int main()
{
	int fd;

	if((mkfifo("./file",0600) == -1) && errno != EEXIST)
	{
		printf("mkfifo failuer\n");
		perror("mkfifo");
	}
	fd = open("./file",O_WRONLY);

	write(fd,"hello",strlen("hello"));

	close(fd);
	return 0;
}

三、消息队列
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。

1、特点

消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。

消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。

消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

2、原型

int msgget(key_t key, int flag);// 创建或打开消息队列:成功返回队列ID,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);// 添加消息:成功返回0,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag); // 读取消息:成功返回消息数据的长度,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);//控制消息队列:成功返回0,失败返回-1

如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。
key参数为IPC_PRIVATE。

key通常用ftok函数获取

key_t ftok(const char *fname,int id); //fname是指定的文件名,id是子序号(只是用8bit 1-255)

如 key = ftok(“.”,1);

例子
write:

struct msgbuf
{
	long mtype;
	char mtext[128];
}


int main()
{
	struct msgbuf sendBuf = {888,"send massage to you"};
	key_t key;
	key = ftok();
	int msgID;
	msgID = msgget(key,IPC_CREAT|0777);
	{
		if(msgID == -1)
		{
			printf("msgget failed\n");
		}
	}	

	msgsnd(msgID,&sendBuf,strlen(sendBuf.mtext),0);

	return 0;
}

read:

int main()
{
	struct msgbuf readBuf;
	key_t key;
	key = ftok();
	int msgID;
	msgID = msgget(key,IPC_CREAT|0777);
	{
		if(msgID == -1)
		{
			printf("msgget failed\n");
		}
	}	
	msgrcv(msgID,&readBuf,sizeof(readBuf.mtext),888,0);
	printf("form send : %s",readBuf.mtext);
	return 0;
}

共享内存
共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。

1、特点

共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。

因为多个进程可以同时操作,所以需要进行同步。

信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

2、原型

int shmget(key_t key, size_t size, int flag);// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
int shmdt(void *addr); // 断开与共享内存的连接:成功返回0,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);// 控制共享内存的相关信息:成功返回0,失败返回-1

当用shmget函数创建一段共享内存时,必须指定其 size;而如果引用一个已存在的共享内存,则将 size 指定为0 。

当一段共享内存被创建以后,它并不能被任何进程访问。必须使用shmat函数连接该共享内存到当前进程的地址空间,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。

shmdt函数是用来断开shmat建立的连接的。注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。

shmctl函数可以对共享内存执行多种操作,根据参数 cmd 执行相应的操作。常用的是IPC_RMID(从系统中删除该共享内存)。

例子:

write

int main()
{
	int shmid;
	key_t key;
	char *shmaddr = NULL;
	key = ftok(".",'z');
	shmid = shmget(key,1024*4,IPC_CREAT|0777);
	shmaddr = shmat(shmid,0,0);

	strcpy(shmaddr,"hello shm");
	sleep(5);

	shmdt(shmaddr);
	shmctl(shmid,IPC_RMID,0);

	return 0;
}

read

int main()
{
	int shmid;
	key_t key;
	char *shmaddr = NULL;
	key = ftok(".",'z');
	shmid = shmget(key,1024*4,0);
	shmaddr = shmat(shmid,0,0);
	printf("get from shm:%s\n,shmaddr);

	shmdt(shmaddr);
	shmctl(shmid,IPC_RMID,0);

	return 0;
}

信号:

对于 Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。信号,为 Linux 提供了一种处理异步事件的方法。比如,终端用户输入了 ctrl+c 来中断程序,会通过信号机制停止一个程序。

信号概述

信号的名字和编号:
每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等。
信号定义在signal.h头文件中,信号名都定义为正整数。
具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0又特殊的应用。

信号的处理:
信号的处理有三种方法,分别是:忽略、捕捉和默认动作

对于信号来说,最大的意义不是为了杀死信号,而是实现一些异步通讯的手段,那么如何来自定义信号的处理函数呢?

信号处理函数的注册

信号处理函数的注册不只一种方法,分为入门版和高级版

入门版:函数signal
高级版:函数sigaction
信号处理发送函数

信号发送函数也不止一个,同样分为入门版和高级版
1.入门版:kill
2.高级版:sigqueue

信号处理函数原型:

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

忽略信号的宏: SIN_IGN
捕捉信号例子:

void handler(int signum)
{
	if(signum == SIGINT)
	{
		printf("signum:%d\n",signum);
		printf("never quit\n");
	}

}

int main()
{
	signal(SIGINT,handler);
	return 0;
}

信号发送函数原型:

int kill(pid_t pid, int sig);

int main(int argc,char *argv[])
{
	int signum;
	int pid;
	pid = atoi(argv[1]);
	signum = atoi(argv[2]);

	kill(pid,signum);
	return 0;
}

高级信号

sigaction 的函数原型

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
   void       (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
   void       (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用
   sigset_t   sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。
   int        sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据
 };
siginfo_t {
               int      si_signo;    /* Signal number */
               int      si_errno;    /* An errno value */
               int      si_code;     /* Signal code */
               int      si_trapno;   /* Trap number that caused
                                        hardware-generated signal
                                        (unused on most architectures) */
               pid_t    si_pid;      /* Sending process ID */
               uid_t    si_uid;      /* Real user ID of sending process */
               int      si_status;   /* Exit value or signal */
               clock_t  si_utime;    /* User time consumed */
               clock_t  si_stime;    /* System time consumed */
               sigval_t si_value;    /* Signal value */
               int      si_int;      /* POSIX.1b signal */
               void    *si_ptr;      /* POSIX.1b signal */
               int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
               int      si_timerid;  /* Timer ID; POSIX.1b timers */
               void    *si_addr;     /* Memory location which caused fault */
               int      si_band;     /* Band event */
               int      si_fd;       /* File descriptor */
}

例子

void handler(int signum , siginfo_t *info, int *done)
{
		union sigval value;
		printf("signum = %d\n",signum);
		printf("pid= %d\n",getpid());

		if(done != NULL)
		{
			printf("from sigqueue: %d",info->si_value.sival_int);
			printf("from sigqueue: %d",info->si_pid);
		}
}

 int main()
 {
 	struct sigaction act;
 	act.sa_sigaction = handler;
 	act.sa_flags = SA_SIGINFO;

 	sigaction(SIGINT1,&act,NULL);
 	return 0;

 }

信号发送函数——高级版

int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
   int   sival_int;
   void *sival_ptr;
}

例子
 int main(int argc,char *argv[])
 {
 	union sigval value;
 	int signum;
 	int pid;

 	signum = atoi(argv[1]);
 	pid = atoi(argv[2]);

 	value.sival_int = 100;
 	sigqueue(pid,signum,value);

 	return 0;
 }

信号量
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

1、特点

信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

支持信号量组。

2、原型

最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。

Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。

int semget(key_t key, int num_sems, int sem_flags);// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);  对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semctl(int semid, int sem_num, int cmd, union semun);// 控制信号量的相关信息

semget参数:
当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为0 。
_semflg :信号量的创建方式或权限。有IPC_CREAT,IPC_EXCL。

semctl参数:
_semid 信号量的标志码(ID),也就是semget()函数的返回值;
_semnum, 操作信号在信号集中的编号。从0开始。
_cmd 命令,表示要进行的操作。
参数cmd中可以使用的命令如下:

IPC_STAT读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
IPC_SET设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
IPC_RMID将信号量集从内存中删除。
GETALL用于读取信号量集中的所有信号量的值。
GETNCNT返回正在等待资源的进程数目。
GETPID返回最后一个执行semop操作的进程的PID。
GETVAL返回信号量集中的一个单个的信号量的值。
GETZCNT返回这在等待完全空闲的资源的进程数目。
SETALL设置信号量集中的所有的信号量的值。
SETVAL设置信号量集中的一个单独的信号量的值。

联合体,用于semctl初始化

union semun
{
  int              val; 
  struct semid_ds *buf;
  unsigned short  *array;
};

在semop函数中,sembuf结构的定义如下:

struct sembuf 
{
     short sem_num; // 信号量组中对应的序号,0~sem_nums-1
     short sem_op;  // 信号量值在一次操作中的改变量
     short sem_flg; // IPC_NOWAIT, SEM_UNDO
 }

sem_num: 操作信号在信号集中的编号。第一个信号的编号为0;

sem_op : 如果其值为正数,该值会加到现有的信号内含值中。通常用于释放所控资源的使用权;如果sem_op的值为负数,而其绝对值又大于信号的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权;如果sem_op的值为0,则操作将暂时阻塞,直到信号的值变为0。

_semflg IPC_NOWAIT //对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。

IPC_UNDO //程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定。

nsops:操作结构的数量,恒大于或等于1。

void psemGet(int semId)
{
	struct sembuf semoparray;
	semoparray.sem_num = 0;
	semoparray.sem_op = -1;
	semoparray.sem_flg = IPC_UNDO;

	semop(semid, &semoparray, 1);
}

void vsemPut(int semId)
{
	struct sembuf semoparray;
	semoparray.sem_num = 0;
	semoparray.sem_op = 1;
	semoparray.sem_flg = IPC_UNDO;

	semop(semid, &semoparray, 1);
}



int main(int argc,char **argv)
{
	pid_t pid;
	key_t key;;
	int semId;
	union semun sem_args;
	sem_args.val = 1;

	key = ftok(".",'z');
	semId = semget(key,1,IPC_CREAT|0666);

	semctl(semId,0,SETVAL,sem_args);

	pid = fork();
	if(pid > 0)
	{
		psemGet(semId);
		printf("this is father\n");
		vsemPut(semId);
	}else if(pid > 0)
	{
		printf("this is child\n");
		vsemPut(semId);
	}

	return 0;
}

线程

典型的UNIX/Linux进程可以看成只有一个控制线程:一个进程在同一时刻只做一件事情。有了多个控制线程后,在程序设计时可以把进程设计成在同一时刻做不止一件事,每个线程各自处理独立的任务。

进程是程序执行时的一个实例,是担当分配系统资源(CPU时间、内存等)的基本单位。在面向线程设计的系统中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程包含了表示进程内执行环境必须的信息,其中包括进程中表示线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno常量以及线程私有数据。进程的所有信息对该进程的所有线程都是共享的,包括可执行的程序文本、程序的全局内存和堆内存、栈以及文件描述符。在Unix和类Unix操作系统中线程也被称为轻量级进程(lightweight processes),但轻量级进程更多指的是内核线程(kernel thread),而把用户线程(user thread)称为线程。

“进程——资源分配的最小单位,线程——程序执行的最小单位”

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,用线程比较方便。

使用线程的理由:

从上面我们知道了进程与线程的区别,其实这些区别也就是我们使用线程的理由。总的来说就是:进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)。

使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

1. 线程创建
函数原型

int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);

// 返回:若成功返回0,否则返回错误编号
当pthread_create成功返回时,由tidp指向的内存单元被设置为新创建线程的线程ID。attr参数用于定制各种不同的线程属性,暂可以把它设置为NULL,以创建默认属性的线程。

新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg。如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。

2. 线程退出

函数原型

int pthread_exit(void *rval_ptr);

rval_ptr是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程可以通过调用pthread_join函数访问到这个指针。
  单个线程可以通过以下三种方式退出,在不终止整个进程的情况下停止它的控制流:

1)线程只是从启动例程中返回,返回值是线程的退出码。

2)线程可以被同一进程中的其他线程取消。

3)线程调用pthread_exit:

3. 线程等待
函数原型

int pthread_join(pthread_t thread, void **rval_ptr);// 返回:若成功返回0,否则返回错误编号

调用这个函数的线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。如果例程只是从它的启动例程返回i,rval_ptr将包含返回码。如果线程被取消,由rval_ptr指定的内存单元就置为PTHREAD_CANCELED。

可以通过调用pthread_join自动把线程置于分离状态,这样资源就可以恢复。如果线程已经处于分离状态,pthread_join调用就会失败,返回EINVAL。

如果对线程的返回值不感兴趣,可以把rval_ptr置为NULL。在这种情况下,调用pthread_join函数将等待指定的线程终止,但并不获得线程的终止状态。

线程的同步

对于多线程程序来说,我们往往需要对这些多线程进行同步。同步(synchronization)是指在一定的时间内只允许某一个线程访问某个资源。而在此时间内,不允许其它的线程访问该资源。我们可以通过互斥锁(mutex),条件变量(condition variable)和读写锁(reader-writer lock)来同步资源。在这里,我们暂不介绍读写锁。

与互斥锁相关API

互斥量(mutex)从本质上来说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为可运行状态的线程可以对互斥量加锁,其他线程将会看到互斥锁依然被锁住,只能回去等待它重新变为可用。在这种方式下,每次只有一个线程可以向前运行。

在设计时需要规定所有的线程必须遵守相同的数据访问规则。只有这样,互斥机制才能正常工作。操作系统并不会做数据访问的串行化。如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其它的线程在使用共享资源前都获取了锁,也还是会出现数据不一致的问题。

互斥变量用pthread_mutex_t数据类型表示。在使用互斥变量前必须对它进行初始化,可以把它置为常量PTHREAD_MUTEX_INITIALIZER(只对静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态地分配互斥量(例如通过调用malloc函数),那么在释放内存前需要调用pthread_mutex_destroy。

  1. 创建及销毁互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t mutex);
// 返回:若成功返回0,否则返回错误编号
  要用默认的属性初始化互斥量,只需把attr设置为NULL。  
  1. 加锁及解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 返回:若成功返回0,否则返回错误编号

如果线程不希望被阻塞,它可以使用pthread_mutex_trylock尝试对互斥量进行加锁。如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞并返回0,否则pthread_mutex_trylock就会失败,不能锁住互斥量,而返回EBUSY。

例子:

int cnt = 0;
pthread_mutex_t mutex;


void Pthread1(void *data)
{
	pthread_mutex_lock(mutex);
	while(1)
	{

		cnt++;
		printf("this is pthread1\n");
		printf("data : %d\n",*((int *)data));
		printf("pthread1:cnt = %d\n");
		sleep(1);
		if(cnt == 3)
		{
			pthread_exit(NULL);
		}
	}

	pthread_mutex_unlock(mutex);
}

void Pthread2(void *data)
{
	while(1)
	{	
		cnt++;
		printf("this is pthread2\n");
		printf("data : %d\n",*((int *)data));
		printf("pthread2:cnt = %d\n");
		sleep(1);
	}

}


int main()
{
	int pram = 100;
	int reg ;
	pthread_t th1;
	pthread_t th2;
	int *exitReg;

	pthread_mutex_init(&mutex,NULL);

	reg = pthread_create(&th1,NULL,(void *)Pthread1,(void *)&pram);
	reg = pthread_create(&th2,NULL,(void *)Pthread1,(void *)&pram);
	if(reg == -1)
	{
		printf("pthread create failed\n");
		exit(-1);
	}

	pthread_join(th1,NULL);
	pthread_join(th2,NULL);

	printf("pthread exit reg : %d",exitReg);
	pthread_mutex_destroy(mutex);
	return 0;
}

信号量的线程控制-互斥和同步

信号量:是操作系统中所用到的PV原语,它广泛用于进程或线程间的同步与互斥,它本质上是一个非 负数的整数计数器
pv:原语是对整数计数器信号量的sem的操作:
一次p操作使 sem减一(分配资源);当sem的值大于或等于0;则上锁成功(拥有访问权)
当sem的值小于0;则阻塞;
而一次 v操作使sem加一(释放资源)
sem_init 用于创建一个信号量,并能初始化它的值。
sem_wait 和 sem_trywait 相当于 P 操作,它们都能将信号量的值减一,两者的区别在于若信号量小于0时,sem_wait 将会阻塞进程,而 sem_trywait 则会立即返回。
sem_post 相当于 V 操作,它将信号量的值加一同时发出信号唤醒等待的进程。
sem_getvalue 用于得到信号量的值。
sem_destroy 用于删除信号量。

7.互斥:同一时刻,只允许一个线程或者进程访问。
用一个信号量 sem =1;
同步 : 多个线程或进程,按一定的顺序执行。
使用到多个信号量。如sem 1 =1; sem = 0;

信号量线程例子:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#include <sys/ipc.h>
#include <semaphore.h>
int lock_var;
time_t end_time;
sem_t sem1,sem2,sem3;

void pthread1(void *arg);
void pthread2(void *arg);
void pthread3(void *arg);


int main(int argc, char *argv[])
{
	pthread_t id1,id2,id3;
	pthread_t mon_th_id;
	int ret;
	end_time = time(NULL)+30;
	ret=sem_init(&sem1,0,1);
	ret=sem_init(&sem2,0,0);
	ret=sem_init(&sem3,0,0);
	if(ret!=0)
	{
		perror("sem_init");
	}
	ret=pthread_create(&id1,NULL,(void *)pthread1, NULL);
	if(ret!=0)
		perror("pthread cread1");
	ret=pthread_create(&id2,NULL,(void *)pthread2, NULL);
	if(ret!=0)
		perror("pthread cread2");

	ret=pthread_create(&id3,NULL,(void *)pthread3, NULL);
	if(ret!=0)
		perror("pthread cread2");
	
	pthread_join(id1,NULL);
	pthread_join(id2,NULL);
	pthread_join(id3,NULL);
	exit(0);
}

void pthread1(void *arg)
{
	int i;
	while(time(NULL) < end_time){
		sem_wait(&sem1);
		for(i=0;i<2;i++){
			sleep(1);
			lock_var++;
			printf("pthread1----lock_var=%d\n",lock_var);
		}
		printf("pthread1:lock_var=%d\n",lock_var);
		sem_post(&sem2);
		sleep(1);
	}
}

void pthread2(void *arg)
{
	int nolock=0;
	int ret;
	while(time(NULL) < end_time){
	sem_wait(&sem2);
	printf("pthread2:pthread2 got lock;lock_var=%d\n",lock_var);
	sem_post(&sem3);
	sleep(3);
	}
}

void pthread3(void *arg)
{
	int nolock=0;
	int ret;
	while(time(NULL) < end_time){
	sem_wait(&sem3);
	printf("pthread3:pthread3 got lock;lock_var=%d\n",lock_var);
	sem_post(&sem1);
	sleep(3);
	}
}

互斥锁什么情况造成死锁
前提条件:俩把锁
进程一和进程二都想获取对方的那把锁,导致谁都无法进行解锁。
死锁的产生需要一定条件:要有一个或多个执行线程和一个或多个资源,每个 线程都在等待其中的一个资源,但所有的资源都已经被占用了,所以线程都在 相互等待,但他们永远不会释放已经占有的资源,于是任何线程都无法继续, 这便意味着死锁的发生。

与条件变量相关API

条件变量是线程另一可用的同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。

条件本身是由互斥量保护的。线程在改变条件状态前必须首先锁住互斥量,其他线程在获得互斥量之前不会察觉到这种改变,因为必须锁定互斥量以后才能计算条件。

条件变量使用之前必须首先初始化,pthread_cond_t数据类型代表的条件变量可以用两种方式进行初始化,可以把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,但是如果条件变量是动态分配的,可以使用pthread_cond_destroy函数对条件变量进行去除初始化(deinitialize)。

1. 创建及销毁条件变量

函数原型

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t cond);
// 返回:若成功返回0,否则返回错误编号
  除非需要创建一个非默认属性的条件变量,否则pthread_cont_init函数的attr参数可以设置为NULL

2. 等待
函数原型

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, cond struct timespec *restrict timeout);
// 返回:若成功返回0,否则返回错误编号

pthread_cond_wait等待条件变为真。如果在给定的时间内条件不能满足,那么会生成一个代表一个出错码的返回变量。传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作都是原子操作。这样就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁住。

pthread_cond_timedwait函数的工作方式与pthread_cond_wait函数类似,只是多了一个timeout。timeout指定了等待的时间,它是通过timespec结构指定。

3. 触发

int pthread_cond_signal(pthread_cond_t cond);
int pthread_cond_broadcast(pthread_cond_t cond);
// 返回:若成功返回0,否则返回错误编号

这两个函数可以用于通知线程条件已经满足。pthread_cond_signal函数将唤醒等待该条件的某个线程,而pthread_cond_broadcast函数将唤醒等待该条件的所有进程。

注意一定要在改变条件状态以后再给线程发信号。

网络编程

TCP/UDP对比

1.TCP面向连接(如打电话要先拨号建立连接);UDP是无线连接的,即发送数据之前,不需要建立连接。
2.TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
3.TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
4.每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5.TCP首部开销20字节;UDP的首部开销小,只有8个字节
6.TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道。

字节序

字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。

1. Little endian:将低序字节存储在起始地址
2. Big endian:将高序字节存储在起始地址

Socket服务器和客户端的开发步骤

1.创建套接字

int socket(int family,int type,int protocol);

2.为套接字添加信息(IP地址和端口号)

struct sockaddr_in {
short int sin_family; /* Internet地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* Internet地址 */
unsigned char sin_zero[8]; /* 添0(和struct sockaddr一样大小)*/
};
int bind(int sockfd,struct sockaddr *my_addr,int addrlen);

3.监听网络连接

listen(int sockfd,int backlog);

4.监听到有客户的接入,接受一个连接

int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);

5.数据交互

read(); write();

6.关闭套接字,断开连接

close();

例子:

服务端

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>



int main(int argc,char *argv[])
{
	int sockfd;
	int sockfd_c;
	struct sockaddr_in s_addr;
	struct sockaddr_in c_addr;
	char sendBuf[128] = "hello";

	sockfd = socket(AF_INET,SOCK_STREAM,0);		
	if(sockfd == -1)
	{
		perror("socket");
		exit(1);
	}
	
	s_addr.sin_family = AF_INET;
	s_addr.sin_port = htons(atoi(argv[2]));
	inet_aton(argv[1],&s_addr.sin_addr);

	bind(sockfd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
	
	listen(sockfd,10);
	
	int clen = sizeof(struct sockaddr_in);
	sockfd_c = accept(sockfd,(struct sockaddr *)&c_addr,&clen);
	if(sockfd_c == -1)
	{
		perror("accept");
		exit(1);
	}

	send(sockfd_c,&sendBuf,128,0);

	return 0;
}

客户端

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>



int main(int argc,char *argv[])
{
	int sockfd_c;
	struct sockaddr_in c_addr;
	int n_recv;
	char recvBuf[128];
	sockfd_c = socket(AF_INET,SOCK_STREAM,0);		
	if(sockfd_c == -1)
	{
		perror("socket");
		exit(1);
	}
	
	c_addr.sin_family = AF_INET;
	c_addr.sin_port = htons(atoi(argv[2]));
	inet_aton(argv[1],&c_addr.sin_addr);

	if(connect(sockfd_c,(struct sockaddr *)&c_addr,sizeof(struct sockaddr_in))==-1)
	{
		perror("connect");
		exit(1);
	}		
	printf("connect success\n");

	n_recv = recv(sockfd_c,&recvBuf,128,0);
	printf("form server : %s\n",recvBuf);

	return 0;
}
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐