kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

信号量内核源码

https://blog.csdn.net/u012603457/article/details/52971894

之前的一片博客介绍了用于Linux内核同步的自旋锁,即使用自旋锁来保护共享资源,今天介绍另外一种Linux内核同步机制——信号量。信号量在内核中的使用非常广泛,用于对各种共享资源的保护。信号量与自旋锁的实现机制是不一样的,用处也是不一样的。首先,自旋锁和信号量都使用了计数器来表示允许同时访问共享资源的最大进程数,但自旋锁的共享计数值是1,也就是说任意时刻只有一个进程在共享代码区运行;信号量却允许使用大于1的共享计数,即共享资源允许被多个不同的进程同时访问,当然,信号量的计数器也能设为1,这时信号量也称为互斥量。其次,自旋锁用于保护短时间能够完成操作的共享资源,使用期间不允许进程睡眠和进程切换;信号量常用于暂时无法获取的共享资源,如果获取失败则进程进入不可中断的睡眠状态,只能由释放资源的进程来唤醒。最后,自旋锁可以用于中断服务程序之中;信号量不能在中断服务程序中使用,因为中断服务程序是不允许进程睡眠的。关于信号量的基本知识已经讲解完毕,接下来看看信号量在内核里面的实现,本文讲解的内核版本是linux-2.6.24。

1 数据结构

1
2
3
4
5
struct semaphore {
	atomic_t count;
	int sleepers;
	wait_queue_head_t wait;
};

信号量使用的数据结构是struct semaphore,包含三个数据成员:count是共享计数值、sleepers是等待当前信号量进入睡眠的进程个数、wait是当前信号量的等待队列。

2 信号量使用

使用信号量之前要进行初始化,其实只是简单的设置共享计数和等待队列,睡眠进程数一开始是0。本文重点讲解信号量的使用和实现。信号量操作的API:

1
2
static inline void down(struct semaphore * sem)//获取信号量,获取失败则进入睡眠状态
static inline void up(struct semaphore * sem)//释放信号量,并唤醒等待队列中的第一个进程

信号量的使用方式如下:

1
2
3
down(sem);
...临界区...
up(sem);

内核保证正在访问临界区的进程数小于或等于初始化的共享计数值,获取信号量失败的进程将进入不可中断的睡眠状态,在信号量的等待队列中进行等待。当进程释放信号量的时候就会唤醒等待队列中的第一个进程。

3 信号量的实现

3.1 down(sem)

首先看函数的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline void down(struct semaphore * sem)
{
	might_sleep();
	__asm__ __volatile__(
		"# atomic down operation\n\t"
		LOCK_PREFIX "decl %0\n\t"  /* --sem->count */
		"jns 2f\n"
		"\tlea %0,%%eax\n\t"
		"call __down_failed\n"
		"2:"
		:"+m" (sem->count)
		:
		:"memory","ax");
}

这里面包含了一些汇编代码,%0代表sem->count。也就是说先将sem->count减1,LOCK_PREFIX表示执行这条指令时将总线锁住,保证减1操作是原子的。减1之后如果大于或等于0就转到标号2处执行,也就跳过了down_failed函数直接到函数尾部并返回,成功获取信号量;否则减1之后sem->count小于0则顺序执行后面的down_failed函数。接下来看__down_failed函数的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ENTRY(__down_failed)
	CFI_STARTPROC
	FRAME
	pushl %edx
	CFI_ADJUST_CFA_OFFSET 4
	CFI_REL_OFFSET edx,0
	pushl %ecx
	CFI_ADJUST_CFA_OFFSET 4
	CFI_REL_OFFSET ecx,0
	call __down
	popl %ecx
	CFI_ADJUST_CFA_OFFSET -4
	CFI_RESTORE ecx
	popl %edx
	CFI_ADJUST_CFA_OFFSET -4
	CFI_RESTORE edx
	ENDFRAME
	ret
	CFI_ENDPROC
	END(__down_failed)

pushl和popl是用于保存和恢复寄存器的,CFI前缀的指令用于指令对齐调整。重点在函数__down,下面来看该函数的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
fastcall void __sched __down(struct semaphore * sem)
{
	struct task_struct *tsk = current;
	DECLARE_WAITQUEUE(wait, tsk);
	unsigned long flags;

	tsk->state = TASK_UNINTERRUPTIBLE;
	spin_lock_irqsave(&sem->wait.lock, flags);
	add_wait_queue_exclusive_locked(&sem->wait, &wait);

	sem->sleepers++;
	for (;;) {
		int sleepers = sem->sleepers;

		/*
		 * Add "everybody else" into it. They aren't
		 * playing, because we own the spinlock in
		 * the wait_queue_head.
		 */
		if (!atomic_add_negative(sleepers - 1, &sem->count)) {
			sem->sleepers = 0;
			break;
		}
		sem->sleepers = 1;  /* us - see -1 above */
		spin_unlock_irqrestore(&sem->wait.lock, flags);

		schedule();

		spin_lock_irqsave(&sem->wait.lock, flags);
		tsk->state = TASK_UNINTERRUPTIBLE;
	}
	remove_wait_queue_locked(&sem->wait, &wait);
	wake_up_locked(&sem->wait);
	spin_unlock_irqrestore(&sem->wait.lock, flags);
	tsk->state = TASK_RUNNING;
}

fastcall表示一种快速调用方式,函数的前两个参数由寄存器ecx和edx来传递,其余参数仍使用堆栈传递。首先将进程设为不可中断睡眠状态,即不能通过信号来唤醒,只能是内核亲自唤醒。同时将进程的TASK_EXCLUSIVE标志设为1,则wake_up()只会唤醒等待队列中的第一个进程。然后将睡眠等待数加1,之后进入for循环。函数atomic_add_negative(sleepers - 1, &sem->count)将相当于sem->count += sleepers-1,然后返回sem->count,通过该函数进行信号量获取情况测试,返回结果为0则获取资源,小于0则没有获取。这段代码使用sleepers和sem->count共同表示当前资源的使用情况。进入for循环后有两种情况,一种是atomic_add_negative执行结果为0,即获取了信号量,此时将sleepers设为0并退出循环,同时唤醒等待队列的第一个进程进行信号量获取测试;另一种是没有获取信号量,将sleepers设为1并运行schedule()进入睡眠,被唤醒之后继续执行for循环进行信号量获取测试。

注意,运行完执行一遍for指令后sleepers的值有两种结果,一种是0,一种是1。如果0则表示有一个进程通过了信号量获取的测试,则atomic_add_negative(sleepers - 1, &sem->count)实际上是将sem->count执行了减1操作,这个操作会在下一个进程进行信号量获取测试的时候执行。如果是1则表示进程没有通过信号领获取的测试,则atomic_add_negative(sleepers - 1, &sem->count)操作不会影响sem->count的值。也就是说,当进程进入__down时,sleepers只会有两个值,一个是0,一个是1。0表示之前的进程获取了信号量,1表示之前的进程没有获取信号量。如果之前进程获取了信号量,执行atomic_add_negative(sleepers - 1, &sem->count)时就会将sem->count的值减1;否则sem->count的值将保持不变。但是这个减1操作延迟到了下一个进程的执行期间,考虑到获取信号量之后进程会唤醒等待队列里的第一个进程,这个减1操作应该会很快就得到执行。

细心地小伙伴可能会注意到,首次获取信号量失败的进程不是会执行sem->sleepers++操作吗,这样不就改变了sem->count的值了吗?仔细回想获取信号量的过程,获取失败的时候会执行sem->count–操作的,因此刚好和sem->sleeper++相互呼应,结果就是不会改变sem->count的结果。即只有进程获取信号量后才会对sem->count进行减1操作,这个操作并不是马上执行,而是后续进程进行信号量获取检测的时候进行的

3.2 up(sem)

先看函数定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
static inline void up(struct semaphore * sem)
{
	__asm__ __volatile__(
		"# atomic up operation\n\t"
		LOCK_PREFIX "incl %0\n\t"  /* ++sem->count */
		"jg 1f\n\t"
		"lea %0,%%eax\n\t"
		"call __up_wakeup\n"
		"1:"
		:"+m" (sem->count)
		:
		:"memory","ax");
}

首先将sem->count加1,是原子操作,如果加1后sem->count大于0则说明没有进程在等待信号量资源,无须唤醒队列中进程,直接跳转到标号1处返回;否则运行__up_wakeup唤醒等待队列中的进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ENTRY(__up_wakeup)
	CFI_STARTPROC
	FRAME
	pushl %edx
	CFI_ADJUST_CFA_OFFSET 4
	CFI_REL_OFFSET edx,0
	pushl %ecx
	CFI_ADJUST_CFA_OFFSET 4
	CFI_REL_OFFSET ecx,0
	call __up
	popl %ecx
	CFI_ADJUST_CFA_OFFSET -4
	CFI_RESTORE ecx
	popl %edx
	CFI_ADJUST_CFA_OFFSET -4
	CFI_RESTORE edx
	ENDFRAME
	ret
	CFI_ENDPROC
	END(__up_wakeup)

同样,我们只关注函数__up的定义:

1
2
3
4
fastcall void __up(struct semaphore *sem)
{
	wake_up(&sem->wait);
}

可以看到,__up的的工作就是唤醒等待队列中的所有进程,但是由于sem等待队列中的进程 的TASK_EXCLUSIVE标志为 1,因此不会唤醒后续进程了。也就是说up(sem)操作实际上是将sem->count自增1,然后唤醒等待队列中的第一个进程(如果有的话)。 4 小结 信号量作为一种基础的内核同步机制,使用非常广泛。本文基于linux-2.6.24内核版本介绍了信号量使用的数据结构和实现机制,同时介绍了信号量与自旋锁的区别。

进程通信--命令行ipcs,ipcrm

内核的 shmall 和 shmmax 参数

/proc/sys/kernel/

shmmax = 配置了最大的内存segment的大小

shmmin = 最小的内存segment的大小

shmmni = 整个系统的内存segment的总个数

shmseg = 每个进程可以使用的内存segment的最大个数


https://www.jb51.net/article/40805.htm

ipcs用法

1
2
3
4
ipcs -a  是默认的输出信息 打印出当前系统中所有的进程间通信方式的信息
ipcs -m  打印出使用共享内存进行进程间通信的信息
ipcs -q  打印出使用消息队列进行进程间通信的信息
ipcs -s  打印出使用信号进行进程间通信的信息

输出格式的控制

1
2
3
4
5
ipcs -t  输出信息的详细变化时间
ipcs -p  输出ipc方式的进程ID
ipcs -c  输出ipc方式的创建者/拥有者
ipcs -c  输出ipc各种方式的在该系统下的限制条件信息
ipcs -u  输出当前系统下ipc各种方式的状态信息(共享内存,消息队列,信号)

ipcrm 命令

移除一个消息对象。或者共享内存段,或者一个信号集,同时会将与ipc对象相关链的数据也一起移除。当然,只有超级管理员,或者ipc对象的创建者才有这项权利啦

ipcrm用法

1
2
3
4
5
6
ipcrm -M shmkey   移除用shmkey创建的共享内存段
ipcrm -m shmid    移除用shmid标识的共享内存段
ipcrm -Q msgkey   移除用msqkey创建的消息队列
ipcrm -q msqid    移除用msqid标识的消息队列
ipcrm -S semkey   移除用semkey创建的信号
ipcrm -s semid    移除用semid标识的信号

进程通信--信号量

https://blog.csdn.net/guoping16/article/details/6584043/

https://www.cnblogs.com/fangshenghui/p/4039946.html

一 为什么要使用信号量

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问 代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它, 也就是说信号量是用来调协进程对共享资源的访问的。其中共享内存的使用就要用到信号量。

二 信号量的工作原理

由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行

V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

三 信号量的使用

1、创建信号量

semget函数创建一个信号量集或访问一个已存在的信号量集。

1
2
#include <sys/sem.h>
int semget (key_t key, int nsem, int oflag);

返回值是一个称为信号量标识符的整数,semop和semctl函数将使用它。

参数nsem指定集合中的信号量数。(若用于访问一个已存在的集合,那就可以把该参数指定为0)

参数oflag可以是SEM_R(read)和SEM_A(alter)常值的组合。(打开时用到),也可以是IPC_CREAT或IPC_EXCL;

2、打开信号量

使用semget打开一个信号量集后,对其中一个或多个信号量的操作就使用semop(op–operate)函数来执行。

1
2
#include <sys/sem.h>
int semop (int semid, struct sembuf * opsptr, size_t nops);

参数opsptr是一个指针,它指向一个信号量操作数组,信号量操作由sembuf结构表示:

1
2
3
4
5
6
7
struct sembuf {
	short sem_num;    // 除非使用一组信号量,否则它为0
	short sem_op; // 信号量在一次操作中需要改变的数据,通常是两个数,
			// 一个是-1,即P(等待)操作,一个是+1,即V(发送信号)操作
	short sem_flg;    // 通常为SEM_UNDO,使操作系统跟踪信号,并在进程没有释放该信号量而终止时,
			// 操作系统释放信号量
};

参数nops规定opsptr数组中元素个数。

sem_op值: (1)若sem_op为正,这对应于进程释放占用的资源数。sem_op值加到信号量的值上。(V操作)
(2)若sem_op为负,这表示要获取该信号量控制的资源数。信号量值减去sem_op的绝对值。(P操作)
(3)若sem_op为0,这表示调用进程希望等待到该信号量值变成0

如果信号量值小于sem_op的绝对值(资源不能满足要求),则:
(1)若指定了IPC_NOWAIT,则semop()出错返回EAGAIN。
(2)若未指定IPC_NOWAIT,则信号量的semncnt值加1(因为调用进程将进 入休眠状态),然后调用进程被挂起直至:①此信号量变成大于或等于sem_op的绝对值;②从系统中删除了此信号量,返回EIDRM;③进程捕捉到一个信 号,并从信号处理程序返回,返回EINTR。(与消息队列的阻塞处理方式 很相似)

3、信号量是操作

semctl函数对一个信号量执行各种控制操作。

1
2
#include <sys/sem.h>
int semctl (int semid, int semnum, int cmd, union semun arg);
参数semid 信号量集标识符
参数semnum 信号量集数组上的下标,表示某一个信号量
参数cmd指定以下10种命令中的一种,在semid指定的信号量集合上执行此命令。
1
2
3
4
5
6
7
8
9
10
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      设置信号量集中的一个单独的信号量的值。
参数 arg
1
2
3
4
5
6
union semun {
	short val;             /* SETVAL用的值 */
	struct semid_ds* buf;  /* IPC_STAT、IPC_SET用的semid_ds结构 */
	unsigned short* array; /* SETALL、GETALL用的数组值 */
	struct seminfo *buf;   /* 为控制IPC_INFO提供的缓存 */
} arg;

四 信号量值的初始化

semget并不初始化各个信号量的值,这个初始化必须通过以SETVAL命令(设置集合中的一个值)或SETALL命令(设置集合中的所有值) 调用semctl来完成。

SystemV信号量的设计中,创建一个信号量集并将它初始化需两次函数调用是一个致命的缺陷。一个不完备的解决方案是:在调用semget时指定IPC_CREAT | IPC_EXCL标志,这样只有一个进程(首先调用semget的那个进程)创建所需信号量,该进程随后初始化该信号量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <time.h>
#include <unistd.h>
#include <sys/wait.h>

#define MAX_SEMAPHORE 10
#define SEMKEY    1

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

int main()
{
	key_t key;
	int semid ,ret, i;
	unsigned short buf[MAX_SEMAPHORE];
	struct sembuf sb[MAX_SEMAPHORE];
	pid_t pid;

	semid = semget(SEMKEY, MAX_SEMAPHORE, IPC_CREAT|0666);
	if (semid == -1) {
		fprintf(stderr, "Error in semget: %s\n", strerror(errno));
		return -1;
	}
	printf("Semaphore get, ID is: %d\n",semid);

	for (i = 0; i < MAX_SEMAPHORE; i++)
		buf[i] = 0;

	arg.array = buf;
	ret = semctl(semid, 0, SETALL, arg);
	if (ret == -1) {
		fprintf(stderr, "Error in semctl: %s\n", strerror(errno));
		return -2;
	}
	printf("Semaphore Init!\n");

	pid = fork();
	if (pid < 0) {
		fprintf(stderr, "Create Process Error!: %s\n",strerror(errno));
		return -3;
	}

	if (pid == 0) {
		sleep(5);
		// 子进程产生信号
		printf("  child wake up.\n");
		for (i = 0; i < MAX_SEMAPHORE; i ++) {
			sb[i].sem_num = i;
			sb[i].sem_op = +1;
			sb[i].sem_flg = 0;
		}

		printf("  child start to inc resource.\n");
		ret = semop(semid, sb, 10);
		if (ret == -1) {
			fprintf(stderr, "子进程产生信号失败: %s\n", strerror(errno));
			exit(-1);
		}
		printf("  child exiting successfully.\n");
		exit(0);
	}

	printf("parent wake up....\n");
	// 此时父进程的阻塞,因为初始化为0
	for (i = 0; i < MAX_SEMAPHORE; i++) {
		sb[i].sem_num = i;
		sb[i].sem_op = -1;
		sb[i].sem_flg = 0;
	}
	printf("parent is asking for resource...\n");
	ret = semop(semid, sb, 10); //p()
	if (ret == 0) {
		printf("parent got the resource!\n");
	}

	// 父进程等待子进程退出
	waitpid(pid, NULL, 0);

	ret = semctl(semid, 0, IPC_RMID);
	if (ret == -1) {
		fprintf(stderr, "semaphore删除失败: %s\n", strerror(errno));
		return -4;
	}

	printf("parent exiting.\n");
	return 0;
}